@hestia-earth/ui-components 0.42.27 → 0.42.28

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,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, ElementRef, output, Directive, DestroyRef, input, computed, HostBinding, Component as Component$1, ChangeDetectionStrategy, Pipe, viewChild, HostListener, model, ViewEncapsulation, signal, effect, contentChild, untracked, viewChildren, forwardRef, LOCALE_ID } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, ElementRef, PLATFORM_ID, output, Directive, DestroyRef, input, computed, HostBinding, Component as Component$1, ChangeDetectionStrategy, Pipe, viewChild, HostListener, model, ViewEncapsulation, signal, effect, contentChild, untracked, viewChildren, forwardRef, LOCALE_ID } from '@angular/core';
3
3
  import { toObservable, toSignal, outputFromObservable, takeUntilDestroyed, rxResource } from '@angular/core/rxjs-interop';
4
4
  import * as i1 from '@angular/forms';
5
5
  import { FormsModule, NG_VALUE_ACCESSOR, UntypedFormBuilder, Validators, ReactiveFormsModule, FormControl } from '@angular/forms';
6
- import { NgClass, DecimalPipe, NgTemplateOutlet, KeyValuePipe, DOCUMENT, PlatformLocation, NgStyle, UpperCasePipe, JsonPipe, DatePipe, AsyncPipe, formatDate as formatDate$1 } from '@angular/common';
6
+ import { isPlatformBrowser, NgClass, DecimalPipe, NgTemplateOutlet, DOCUMENT, KeyValuePipe, PlatformLocation, NgStyle, UpperCasePipe, JsonPipe, DatePipe, AsyncPipe, formatDate as formatDate$1 } from '@angular/common';
7
7
  import * as i1$1 from '@ng-bootstrap/ng-bootstrap';
8
8
  import { NgbTooltip, NgbDropdown, NgbDropdownMenu, NgbDropdownToggle, NgbActiveModal, NgbHighlight, NgbModal, NgbDropdownItem, NgbTypeahead, NgbPopover, NgbTooltipModule, NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
9
- import { ReplaySubject, of, mergeMap, shareReplay, delay, map, catchError, distinctUntilChanged, timer, take, first, Subject, combineLatest, filter, fromEvent, startWith, merge, skip, switchMap, EMPTY, throttleTime, animationFrameScheduler, debounceTime, takeUntil, lastValueFrom, tap, skipUntil, zip, mergeAll, reduce, forkJoin, throwError, from, firstValueFrom, toArray, distinct, groupBy } from 'rxjs';
9
+ import { ReplaySubject, of, mergeMap, shareReplay, delay, map, catchError, distinctUntilChanged, timer, take, first, EMPTY, Subject, combineLatest, filter, fromEvent, startWith, merge, skip, switchMap, throttleTime, animationFrameScheduler, debounceTime, takeUntil, lastValueFrom, tap, skipUntil, zip, mergeAll, reduce, forkJoin, throwError, from, firstValueFrom, toArray, distinct, groupBy } from 'rxjs';
10
10
  import { HttpClient } from '@angular/common/http';
11
11
  import get from 'lodash.get';
12
12
  import { SCHEMA_VERSION, SchemaType, NodeType, TermTermType, productTermTermType, nestedSearchableKeys, SiteSiteType, EmissionMethodTier, isExpandable, sortKeysByType, isTypeNode, uniquenessFields, BlankNodesKey, impactAssessmentTermTermType, measurementTermTermType, emissionTermTermType, inputTermTermType, CycleFunctionalUnit, NonBlankNodesKey, jsonldPath, isTypeValid, typeToSchemaType, infrastructureTermTermType, managementTermTermType } from '@hestia-earth/schema';
@@ -76,16 +76,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
76
76
 
77
77
  const gitHome = 'https://gitlab.com/hestia-earth';
78
78
  const gitBranch = () => (['dev', 'staging'].some(env => baseUrl().includes(env)) ? 'develop' : 'master');
79
- const isChrome = () => window.navigator.userAgent.includes('Chrome');
80
- const baseUrl = (allowLocalhost = true) => window.location.origin?.includes('localhost')
79
+ /**
80
+ * Access `window` defensively so this module can be evaluated outside the browser
81
+ * (SSR / prerender / Node). On the server there is no location, so callers fall
82
+ * back to production defaults.
83
+ */
84
+ const windowRef = () => (typeof window === 'undefined' ? undefined : window);
85
+ const origin = () => windowRef()?.location?.origin ?? '';
86
+ const isChrome = () => windowRef()?.navigator?.userAgent?.includes('Chrome') ?? false;
87
+ const baseUrl = (allowLocalhost = true) => origin().includes('localhost')
81
88
  ? allowLocalhost
82
- ? window.location.origin
89
+ ? origin()
83
90
  : 'https://www-dev.hestia.earth'
84
- : window.location.origin?.includes('hestia.earth')
85
- ? window.location.origin
91
+ : origin().includes('hestia.earth')
92
+ ? origin()
86
93
  : 'https://www.hestia.earth';
87
94
  const baseApiUrl = () => baseUrl(false).replace('www', 'api');
88
- const isExternal = () => baseUrl() !== window.location.origin;
95
+ const isExternal = () => baseUrl() !== origin();
89
96
  const schemaBaseUrl = (version) => [baseUrl(), 'schema', version].filter(Boolean).join('/');
90
97
  const schemaDataBaseUrl = (version) => [baseUrl(false), 'schema-data', version || SCHEMA_VERSION].filter(Boolean).join('/');
91
98
  const glossaryBaseUrl = (allowLocalhost = true) => [baseUrl(allowLocalhost), 'glossary'].filter(Boolean).join('/');
@@ -120,7 +127,11 @@ const filterParams = (obj) => {
120
127
  });
121
128
  return res;
122
129
  };
123
- const waitFor = (variable, callback) => get(window, variable, false) ? callback() : setTimeout(() => waitFor(variable, callback), 100);
130
+ const waitFor = (variable, callback) => typeof window === 'undefined'
131
+ ? undefined
132
+ : get(window, variable, false)
133
+ ? callback()
134
+ : setTimeout(() => waitFor(variable, callback), 100);
124
135
  const bottom = (element) => element.offsetTop + element.getBoundingClientRect().height;
125
136
  const isScrolledBelow = (element) => (element ? window.scrollY > bottom(element) : false);
126
137
  const scrollToEl = (id, args = {
@@ -1545,14 +1556,18 @@ class ResizedEvent {
1545
1556
  class ResizedDirective {
1546
1557
  constructor() {
1547
1558
  this.element = inject(ElementRef);
1559
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1548
1560
  this.resized = output();
1549
- this.observer = new ResizeObserver(entries => this.observe(entries));
1561
+ // `ResizeObserver` is browser-only; there are no resize events during SSR.
1562
+ if (this.isBrowser) {
1563
+ this.observer = new ResizeObserver(entries => this.observe(entries));
1564
+ }
1550
1565
  }
1551
1566
  ngOnInit() {
1552
- this.observer.observe(this.element.nativeElement);
1567
+ this.observer?.observe(this.element.nativeElement);
1553
1568
  }
1554
1569
  ngOnDestroy() {
1555
- this.observer.disconnect();
1570
+ this.observer?.disconnect();
1556
1571
  }
1557
1572
  observe(entries) {
1558
1573
  const domSize = entries[0];
@@ -1574,6 +1589,10 @@ const distinctUntilChangedDeep = () => distinctUntilChanged((x, y) => isEqual$1(
1574
1589
  const takeAfterViewInit = (fn, settings = { take: 10 }) => timer(0, 50).pipe(take(settings.take), map(() => fn()), first(returnItem => !!returnItem));
1575
1590
  const injectResizeEvent$ = (elementRef) => {
1576
1591
  const destroyRef = inject(DestroyRef);
1592
+ // `ResizeObserver` is browser-only; there are no resize events during SSR.
1593
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) {
1594
+ return EMPTY;
1595
+ }
1577
1596
  const resizedEvent$ = new Subject();
1578
1597
  let oldRect;
1579
1598
  const observer = new ResizeObserver(entries => {
@@ -1723,7 +1742,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
1723
1742
 
1724
1743
  const GUIDE_ENABLED = new InjectionToken('HESTIA_GUIDE_ENABLED');
1725
1744
  const guideNamespace = 'he-guide';
1726
- const postGuideEvent = (data) => window.parent.postMessage({ namespace: guideNamespace, ...data }, '*');
1745
+ const postGuideEvent = (data) => typeof window === 'undefined'
1746
+ ? undefined
1747
+ : window.parent.postMessage({ namespace: guideNamespace, ...data }, '*');
1727
1748
  const handleGuideEvent = (handler) => (event) => {
1728
1749
  const data = event.data;
1729
1750
  if (data.namespace == guideNamespace) {
@@ -2029,7 +2050,10 @@ const toBreakpoint = ({ min, max }) => [min ? toSize(beakpointWidths[min], 'min'
2029
2050
  class ResponsiveService {
2030
2051
  constructor() {
2031
2052
  this.breakPointObserver = inject(BreakpointObserver);
2032
- this.windowWidth$ = fromEvent(window, 'resize').pipe(startWith(window.innerWidth), map(() => window.innerWidth));
2053
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2054
+ this.windowWidth$ = this.isBrowser
2055
+ ? fromEvent(window, 'resize').pipe(startWith(window.innerWidth), map(() => window.innerWidth))
2056
+ : of(0);
2033
2057
  this.isMobile$ = this.breakPointObserver.observe(toBreakpoint({ max: Breakpoint.tablet })).pipe(map(state => state.matches), distinctUntilChanged());
2034
2058
  this.isTablet$ = this.breakPointObserver
2035
2059
  .observe(toBreakpoint({ min: Breakpoint.tablet, max: Breakpoint.desktop }))
@@ -2047,7 +2071,7 @@ class ResponsiveService {
2047
2071
  */
2048
2072
  this.is1080p$ = this.breakPointObserver.observe(toSize(1920, 'min')).pipe(map(state => state.matches), distinctUntilChanged());
2049
2073
  this.isRetinaDisplay = () => {
2050
- if (window.matchMedia) {
2074
+ if (this.isBrowser && window.matchMedia) {
2051
2075
  const mq = window.matchMedia([
2052
2076
  'min--moz-device-pixel-ratio: 1.3',
2053
2077
  '-o-min-device-pixel-ratio: 2.6/2',
@@ -2117,6 +2141,8 @@ class DrawerContainerComponent {
2117
2141
  constructor() {
2118
2142
  this.responsiveService = inject(ResponsiveService);
2119
2143
  this.localStorage = inject(LocalStorageService);
2144
+ this.document = inject(DOCUMENT);
2145
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2120
2146
  this.contentContainer = viewChild('contentContainer', { ...(ngDevMode ? { debugName: "contentContainer" } : {}), read: ElementRef });
2121
2147
  this.menuState = signal('close', ...(ngDevMode ? [{ debugName: "menuState" }] : []));
2122
2148
  /**
@@ -2199,7 +2225,9 @@ class DrawerContainerComponent {
2199
2225
  this.destroy$ = new Subject();
2200
2226
  this._updateSidenavEvent$ = toObservable(this.isResizing).pipe(switchMap(listen => (listen ? moveEvent() : EMPTY)), throttleTime(0, animationFrameScheduler));
2201
2227
  this._contentContainer$ = toObservable(this.contentContainer).pipe(filter(v => !!v));
2202
- this._hostComponentWidth$ = fromEvent(window, 'resize').pipe(startWith(window.innerWidth));
2228
+ this._hostComponentWidth$ = this.isBrowser
2229
+ ? fromEvent(window, 'resize').pipe(startWith(window.innerWidth))
2230
+ : of(0);
2203
2231
  /**
2204
2232
  * we use combineLatest instead of computed to avoid the animation flickering (wait for all sources to emit)
2205
2233
  */
@@ -2221,13 +2249,13 @@ class DrawerContainerComponent {
2221
2249
  effect(() => {
2222
2250
  const position = this.position();
2223
2251
  const width = this.sidenavWidth();
2224
- document.body.style.setProperty(`--sidenav-${position}-width`, `${width}px`);
2252
+ this.document.body.style.setProperty(`--sidenav-${position}-width`, `${width}px`);
2225
2253
  });
2226
2254
  effect(() => {
2227
2255
  const expanded = this.expanded();
2228
2256
  const position = this.position();
2229
2257
  const value = this.contentContainerTransform();
2230
- document.body.style.setProperty(`--content-transition-${position}-x`, `${expanded ? value : 0}px`);
2258
+ this.document.body.style.setProperty(`--content-transition-${position}-x`, `${expanded ? value : 0}px`);
2231
2259
  });
2232
2260
  // initial load of configuration
2233
2261
  effect(() => this.storageKey() && this.storeConfiguration());
@@ -2304,6 +2332,8 @@ class DrawerContainerComponent {
2304
2332
  this.menuState.set(didOpen ? 'open' : 'close');
2305
2333
  }
2306
2334
  _getContentLeftSideSpace(element) {
2335
+ if (!this.isBrowser)
2336
+ return 0;
2307
2337
  const position = this.position();
2308
2338
  const styles = window.getComputedStyle(element.nativeElement, null);
2309
2339
  const padding = styles.getPropertyValue('padding-' + position);
@@ -3006,6 +3036,12 @@ const loadSvgSprite = () => {
3006
3036
  const http = inject(HttpClient);
3007
3037
  const document = inject(DOCUMENT);
3008
3038
  const platformLocation = inject(PlatformLocation);
3039
+ // The sprite is injected into the DOM for the browser to render `<use>` refs.
3040
+ // During SSR/prerender there is no asset server to fetch it from, and the
3041
+ // client re-injects it on hydration, so skip the fetch on the server.
3042
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) {
3043
+ return Promise.resolve();
3044
+ }
3009
3045
  const baseUrl = platformLocation.getBaseHrefFromDOM();
3010
3046
  return lastValueFrom(http.get(`${baseUrl}assets/svg-icons/icons-sprite.svg`, { responseType: 'text' }).pipe(tap(svgContent => {
3011
3047
  const div = document.createElement('div');
@@ -3464,10 +3500,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3464
3500
  class SocialTagsComponent {
3465
3501
  constructor() {
3466
3502
  this.meta = inject(Meta);
3503
+ this.document = inject(DOCUMENT);
3467
3504
  this.config = input({}, ...(ngDevMode ? [{ debugName: "config" }] : []));
3468
3505
  this.classes = 'is-hidden';
3469
3506
  this.configs = computed(() => ({
3470
- 'og:url': window.location.href.split('?')[0],
3507
+ 'og:url': this.document.location.href.split('?')[0],
3471
3508
  ...this.config
3472
3509
  }), ...(ngDevMode ? [{ debugName: "configs" }] : []));
3473
3510
  this.meta.addTag({ charset: 'UTF-8' });
@@ -3562,8 +3599,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3562
3599
  /* eslint-disable @angular-eslint/directive-selector */
3563
3600
  class DocumentClickService {
3564
3601
  constructor() {
3602
+ this.document = inject(DOCUMENT);
3565
3603
  this.documentClick$ = new Subject();
3566
- fromEvent(document, 'click')
3604
+ // Document click events only exist in the browser; skip on the server (SSR).
3605
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) {
3606
+ return;
3607
+ }
3608
+ fromEvent(this.document, 'click')
3567
3609
  .pipe(takeUntilDestroyed(), tap(event => this.documentClick$.next(event)))
3568
3610
  .subscribe();
3569
3611
  }
@@ -8933,16 +8975,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
8933
8975
 
8934
8976
  class GuideOverlayService {
8935
8977
  constructor() {
8978
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
8936
8979
  this.message = signal(null, ...(ngDevMode ? [{ debugName: "message" }] : []));
8937
8980
  this.messageListener = ({ data }) => {
8938
8981
  if (data.namespace == guideNamespace) {
8939
8982
  this.message.set(data);
8940
8983
  }
8941
8984
  };
8942
- window.addEventListener('message', this.messageListener);
8985
+ // `postMessage` communication only exists in the browser; skip on the server (SSR).
8986
+ if (this.isBrowser) {
8987
+ window.addEventListener('message', this.messageListener);
8988
+ }
8943
8989
  }
8944
8990
  ngOnDestroy() {
8945
- window.removeEventListener('message', this.messageListener);
8991
+ if (this.isBrowser) {
8992
+ window.removeEventListener('message', this.messageListener);
8993
+ }
8946
8994
  }
8947
8995
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: GuideOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
8948
8996
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: GuideOverlayService, providedIn: 'root' }); }
@@ -9290,6 +9338,10 @@ const isMissing = (value) => value === undefined || value === null || value ===
9290
9338
  // the result symbol (left-hand side) - it is the model's output, not an input that "was not logged", so
9291
9339
  // it is excluded from the missing-values notice (e.g. a failed model has no result value)
9292
9340
  const resultKey = 'value';
9341
+ // the reserved table key a `\sum` over per-contributor products binds to (e.g. `[contributions:value]`).
9342
+ // its rows come from the ImpactAssessment's contribution data (register_contribution), not the jlog, so
9343
+ // these "characterisation = Σ contributions" models render fully without duplicating values into the logs
9344
+ const contributionsKey = 'contributions';
9293
9345
  // the "value formula" computes the model's result (`key: "value"`); any others are sub-formulas that
9294
9346
  // compute its components (possibly chained: a sub-formula's result feeds another sub-formula)
9295
9347
  const isValueFormula = (formula) => formula.bindings.some(binding => binding.key === resultKey);
@@ -9352,15 +9404,42 @@ class NodeLogsModelsFormulaComponent {
9352
9404
  // the value the model returned (the recalculated node value): the result symbol (`key: "value"`) is
9353
9405
  // usually not logged, so it falls back to this
9354
9406
  this.value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : []));
9407
+ // the ImpactAssessment the model ran on: lets a contribution-based `\sum` pull its per-contributor
9408
+ // summands from the stored contribution data instead of duplicating them into the jlog
9409
+ this.node = input(...(ngDevMode ? [undefined, { debugName: "node" }] : []));
9410
+ this.nodeService = inject(HeNodeService);
9355
9411
  // show the substituted formula (values in place of symbols) rather than the raw symbolic one
9356
9412
  this.substituted = signal(false, ...(ngDevMode ? [{ debugName: "substituted" }] : []));
9357
9413
  // a unique id per instance so the switch label only toggles its own checkbox (several popovers can co-exist)
9358
9414
  this.toggleId = uuid('formulaSubstituted-');
9359
9415
  this.formulas = computed(() => getModelFormulas(this.model()), ...(ngDevMode ? [{ debugName: "formulas" }] : []));
9416
+ // whether any formula sums over per-contributor products (bound to the reserved `contributions` table)
9417
+ this.hasContributionBinding = computed(() => this.formulas().some(formula => formula.bindings.some(binding => binding.key === contributionsKey)), ...(ngDevMode ? [{ debugName: "hasContributionBinding" }] : []));
9418
+ // only fetch contributions when a formula actually needs them and we have an ImpactAssessment to fetch for
9419
+ this.contributionResource = rxResource({
9420
+ params: () => (this.hasContributionBinding() && this.node() ? { node: this.node() } : undefined),
9421
+ stream: ({ params: { node } }) => this.nodeService.getContributions$(node)
9422
+ });
9423
+ // the contributions to this indicator term, grouped by method model (empty until loaded / when N/A)
9424
+ this.termContributions = computed(() => {
9425
+ const node = this.node();
9426
+ const contributions = this.contributionResource.value();
9427
+ if (!node || !contributions)
9428
+ return {};
9429
+ return simplifyContributions(node, contributions)[this.model()?.term ?? ''] ?? {};
9430
+ }, ...(ngDevMode ? [{ debugName: "termContributions" }] : []));
9431
+ // the per-contributor summands for this model, shaped as `\sum` rows: { id: contributor term, value }
9432
+ this.contributionRows = computed(() => {
9433
+ const byIndicator = this.termContributions()[this.model()?.model ?? ''] ?? {};
9434
+ return Object.entries(byIndicator).map(([id, value]) => ({ id, value }));
9435
+ }, ...(ngDevMode ? [{ debugName: "contributionRows" }] : []));
9360
9436
  this.values = computed(() => {
9361
9437
  const values = logValues(this.logs());
9362
9438
  // the result (`value`) is usually the model's returned value rather than a logged field
9363
- return isMissing(values.value) && !isMissing(this.value()) ? { ...values, value: this.value() } : values;
9439
+ const withResult = isMissing(values.value) && !isMissing(this.value()) ? { ...values, value: this.value() } : values;
9440
+ // feed a contribution-based `\sum` from the stored contributions (nothing to merge for other formulas)
9441
+ const rows = this.contributionRows();
9442
+ return rows.length ? { ...withResult, [contributionsKey]: rows } : withResult;
9364
9443
  }, ...(ngDevMode ? [{ debugName: "values" }] : []));
9365
9444
  this.valueFormula = computed(() => this.formulas().find(isValueFormula), ...(ngDevMode ? [{ debugName: "valueFormula" }] : []));
9366
9445
  // the value formula's own symbol keys - kept symbolic in the sub-formulas (they are substituted only in
@@ -9411,12 +9490,12 @@ class NodeLogsModelsFormulaComponent {
9411
9490
  this.substituted.update(value => !value);
9412
9491
  }
9413
9492
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NodeLogsModelsFormulaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
9414
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: NodeLogsModelsFormulaComponent, isStandalone: true, selector: "he-node-logs-models-formula", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, logs: { classPropertyName: "logs", publicName: "logs", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (formulas().length) {\n <div class=\"formula-block is-mb-2 has-text-white\">\n <div class=\"is-flex is-align-items-center is-justify-content-space-between is-gap-8 is-mb-1 is-size-8\">\n <span class=\"is-uppercase has-text-weight-semibold\">Formula{{ formulas().length > 1 ? 's' : '' }}</span>\n <div class=\"field\">\n <input\n type=\"checkbox\"\n class=\"switch is-small is-rounded\"\n [id]=\"toggleId\"\n [checked]=\"substituted()\"\n [disabled]=\"!hasSubstitutions()\"\n (change)=\"toggle()\" />\n <label [for]=\"toggleId\" [title]=\"hasSubstitutions() ? '' : 'No logged values to substitute'\">\n <span>{{ substituted() ? 'Substituted' : 'Raw' }}</span>\n </label>\n </div>\n </div>\n\n @for (item of renderedFormulas(); track $index) {\n <div class=\"formula\" [heKatex]=\"item.rendered\"></div>\n\n @if (item.variables.length) {\n <ul class=\"is-size-7 is-mt-1 is-mb-2 is-list-style-disc | formula-variables\">\n @for (variable of item.variables; track $index) {\n <li>\n <div class=\"is-flex is-align-items-baseline is-gap-4\">\n <span class=\"formula-variable-symbol is-nowrap\">\n <span [heKatex]=\"variable.symbol\" [heKatexInline]=\"true\"></span>\n @if (variable.description) {\n <span>:</span>\n }\n </span>\n @if (variable.description) {\n <markdown class=\"is-inline-block is-italic | formula-variable-desc\" [data]=\"variable.description\" />\n }\n @if (variable.missing) {\n <span class=\"has-text-warning\">(missing)</span>\n }\n </div>\n </li>\n }\n </ul>\n }\n }\n </div>\n}\n", styles: [".formula-block{border-bottom:1px solid rgba(255,255,255,.2)}.formula-variable-symbol{flex-shrink:0}.formula-variable-symbol ::ng-deep .katex{font-size:1em}.formula-variable-desc{opacity:.85}.formula-variable-desc ::ng-deep *{display:inline;margin:0}.formula{overflow-x:auto;overflow-y:hidden}.formula ::ng-deep .katex-display{margin:.35rem 0}\n"], dependencies: [{ kind: "directive", type: KatexDirective, selector: "[heKatex]", inputs: ["heKatex", "heKatexInline"] }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9493
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: NodeLogsModelsFormulaComponent, isStandalone: true, selector: "he-node-logs-models-formula", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, logs: { classPropertyName: "logs", publicName: "logs", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (formulas().length) {\n <div class=\"formula-block is-mb-2 has-text-white\">\n <div class=\"is-flex is-align-items-center is-justify-content-space-between is-gap-8 is-mb-1 is-size-8\">\n <span class=\"is-uppercase has-text-weight-semibold\">Formula{{ formulas().length > 1 ? 's' : '' }}</span>\n <div class=\"field\">\n <input\n type=\"checkbox\"\n class=\"switch is-small is-rounded\"\n [id]=\"toggleId\"\n [checked]=\"substituted()\"\n [disabled]=\"!hasSubstitutions()\"\n (change)=\"toggle()\" />\n <label [for]=\"toggleId\" [title]=\"hasSubstitutions() ? '' : 'No logged values to substitute'\">\n <span>{{ substituted() ? 'Substituted' : 'Raw' }}</span>\n </label>\n </div>\n </div>\n\n @for (item of renderedFormulas(); track $index) {\n <div class=\"formula\" [heKatex]=\"item.rendered\"></div>\n\n @if (item.variables.length) {\n <ul class=\"is-size-7 is-mt-1 is-mb-2 is-list-style-disc | formula-variables\">\n @for (variable of item.variables; track $index) {\n <li>\n <div class=\"is-flex is-align-items-baseline is-gap-4\">\n <span class=\"formula-variable-symbol is-nowrap\">\n <span [heKatex]=\"variable.symbol\" [heKatexInline]=\"true\"></span>\n @if (variable.description) {\n <span>:</span>\n }\n </span>\n @if (variable.description) {\n <markdown class=\"is-inline-block is-italic | formula-variable-desc\" [data]=\"variable.description\" />\n }\n @if (variable.missing) {\n <span class=\"has-text-warning\">(missing)</span>\n }\n </div>\n </li>\n }\n </ul>\n }\n }\n </div>\n}\n", styles: [".formula-block{border-bottom:1px solid rgba(255,255,255,.2)}.formula-variable-symbol{flex-shrink:0}.formula-variable-symbol ::ng-deep .katex{font-size:1em}.formula-variable-desc{opacity:.85}.formula-variable-desc ::ng-deep *{display:inline;margin:0}.formula{overflow-x:auto;overflow-y:hidden}.formula ::ng-deep .katex-display{margin:.35rem 0}\n"], dependencies: [{ kind: "directive", type: KatexDirective, selector: "[heKatex]", inputs: ["heKatex", "heKatexInline"] }, { kind: "component", type: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9415
9494
  }
9416
9495
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NodeLogsModelsFormulaComponent, decorators: [{
9417
9496
  type: Component$1,
9418
9497
  args: [{ selector: 'he-node-logs-models-formula', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KatexDirective, MarkdownComponent], template: "@if (formulas().length) {\n <div class=\"formula-block is-mb-2 has-text-white\">\n <div class=\"is-flex is-align-items-center is-justify-content-space-between is-gap-8 is-mb-1 is-size-8\">\n <span class=\"is-uppercase has-text-weight-semibold\">Formula{{ formulas().length > 1 ? 's' : '' }}</span>\n <div class=\"field\">\n <input\n type=\"checkbox\"\n class=\"switch is-small is-rounded\"\n [id]=\"toggleId\"\n [checked]=\"substituted()\"\n [disabled]=\"!hasSubstitutions()\"\n (change)=\"toggle()\" />\n <label [for]=\"toggleId\" [title]=\"hasSubstitutions() ? '' : 'No logged values to substitute'\">\n <span>{{ substituted() ? 'Substituted' : 'Raw' }}</span>\n </label>\n </div>\n </div>\n\n @for (item of renderedFormulas(); track $index) {\n <div class=\"formula\" [heKatex]=\"item.rendered\"></div>\n\n @if (item.variables.length) {\n <ul class=\"is-size-7 is-mt-1 is-mb-2 is-list-style-disc | formula-variables\">\n @for (variable of item.variables; track $index) {\n <li>\n <div class=\"is-flex is-align-items-baseline is-gap-4\">\n <span class=\"formula-variable-symbol is-nowrap\">\n <span [heKatex]=\"variable.symbol\" [heKatexInline]=\"true\"></span>\n @if (variable.description) {\n <span>:</span>\n }\n </span>\n @if (variable.description) {\n <markdown class=\"is-inline-block is-italic | formula-variable-desc\" [data]=\"variable.description\" />\n }\n @if (variable.missing) {\n <span class=\"has-text-warning\">(missing)</span>\n }\n </div>\n </li>\n }\n </ul>\n }\n }\n </div>\n}\n", styles: [".formula-block{border-bottom:1px solid rgba(255,255,255,.2)}.formula-variable-symbol{flex-shrink:0}.formula-variable-symbol ::ng-deep .katex{font-size:1em}.formula-variable-desc{opacity:.85}.formula-variable-desc ::ng-deep *{display:inline;margin:0}.formula{overflow-x:auto;overflow-y:hidden}.formula ::ng-deep .katex-display{margin:.35rem 0}\n"] }]
9419
- }], propDecorators: { model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: false }] }], logs: [{ type: i0.Input, args: [{ isSignal: true, alias: "logs", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
9498
+ }], propDecorators: { model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: false }] }], logs: [{ type: i0.Input, args: [{ isSignal: true, alias: "logs", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }] } });
9420
9499
 
9421
9500
  class NodeLogsModelsDetailsComponent {
9422
9501
  constructor() {
@@ -9460,7 +9539,7 @@ class NodeLogsModelsDetailsComponent {
9460
9539
  this.openKeys.set(next);
9461
9540
  }
9462
9541
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NodeLogsModelsDetailsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
9463
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: NodeLogsModelsDetailsComponent, isStandalone: true, selector: "he-node-logs-models-details", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, nodeKey: { classPropertyName: "nodeKey", publicName: "nodeKey", isSignal: true, isRequired: false, transformFunction: null }, hasContributions: { classPropertyName: "hasContributions", publicName: "hasContributions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@for (section of sections(); track section.key; let last = $last) {\n <div class=\"log-section\" [class.has-border-bottom]=\"!last || isOpen(section.key)\">\n @if (section.key === 'formula' && isOpen('formula')) {\n <!-- the formula component renders its own \"Formula BETA\" title + Raw/Substituted switch on one\n line, so keep the toggle chevron inline with it rather than adding a duplicate header -->\n <div class=\"log-section-header is-flex is-align-items-flex-start is-gap-4\">\n <a class=\"has-text-white\" (click)=\"toggle('formula')\">\n <he-svg-icon name=\"chevron-down\" size=\"16\" />\n </a>\n <div class=\"is-flex-grow-1\">\n <he-node-logs-models-formula [model]=\"model().model\" [logs]=\"model().logs\" [value]=\"value()\" />\n </div>\n </div>\n } @else {\n <a\n class=\"log-section-header is-flex is-align-items-center is-gap-4 has-text-white is-size-8 is-uppercase has-text-weight-semibold\"\n (click)=\"toggle(section.key)\">\n <he-svg-icon [name]=\"isOpen(section.key) ? 'chevron-down' : 'chevron-right'\" size=\"16\" />\n <span>{{ section.label }}</span>\n </a>\n\n @if (isOpen(section.key)) {\n <div class=\"log-section-body is-pb-2\">\n @switch (section.key) {\n @case ('logs') {\n <he-node-logs-models-logs [logs]=\"model().logs\" [renderRaw]=\"false\" />\n }\n @case ('raw') {\n <div class=\"is-flex is-justify-content-flex-end is-mb-1\">\n <he-clipboard clipboardClass=\"is-size-7 is-p-1\" [value]=\"rawJson()\" [hideText]=\"true\" />\n </div>\n <pre class=\"raw-log\">{{ rawJson() }}</pre>\n }\n @case ('contributions') {\n <he-node-logs-models-contributions [node]=\"node()\" [nodeKey]=\"nodeKey()\" [model]=\"model()\" />\n }\n }\n </div>\n }\n }\n </div>\n}\n", styles: [".log-section{border-color:#fff3!important}.log-section-header{cursor:pointer;padding:.5rem 0}.log-section-header ::ng-deep .formula-block{margin-bottom:0;border-bottom:none}.raw-log{max-height:300px;overflow:auto;white-space:pre;font-size:.75rem;background:#00000059;color:inherit;padding:.5rem;border-radius:3px}\n"], dependencies: [{ kind: "component", type: HESvgIconComponent, selector: "he-svg-icon", inputs: ["name", "size", "animation"] }, { kind: "component", type: ClipboardComponent, selector: "he-clipboard", inputs: ["icon", "value", "disabled", "hideText", "hideIcon", "size", "clipboardClass", "tooltipPlacement"] }, { kind: "component", type: NodeLogsModelsFormulaComponent, selector: "he-node-logs-models-formula", inputs: ["model", "logs", "value"] }, { kind: "component", type: NodeLogsModelsLogsComponent, selector: "he-node-logs-models-logs", inputs: ["logs", "renderRaw"] }, { kind: "component", type: NodeLogsModelsContributionsComponent, selector: "he-node-logs-models-contributions", inputs: ["node", "nodeKey", "model"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9542
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: NodeLogsModelsDetailsComponent, isStandalone: true, selector: "he-node-logs-models-details", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, nodeKey: { classPropertyName: "nodeKey", publicName: "nodeKey", isSignal: true, isRequired: false, transformFunction: null }, hasContributions: { classPropertyName: "hasContributions", publicName: "hasContributions", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@for (section of sections(); track section.key; let last = $last) {\n <div class=\"log-section\" [class.has-border-bottom]=\"!last || isOpen(section.key)\">\n @if (section.key === 'formula' && isOpen('formula')) {\n <!-- the formula component renders its own \"Formula BETA\" title + Raw/Substituted switch on one\n line, so keep the toggle chevron inline with it rather than adding a duplicate header -->\n <div class=\"log-section-header is-flex is-align-items-flex-start is-gap-4\">\n <a class=\"has-text-white\" (click)=\"toggle('formula')\">\n <he-svg-icon name=\"chevron-down\" size=\"16\" />\n </a>\n <div class=\"is-flex-grow-1\">\n <he-node-logs-models-formula\n [model]=\"model().model\"\n [logs]=\"model().logs\"\n [value]=\"value()\"\n [node]=\"node()\" />\n </div>\n </div>\n } @else {\n <a\n class=\"log-section-header is-flex is-align-items-center is-gap-4 has-text-white is-size-8 is-uppercase has-text-weight-semibold\"\n (click)=\"toggle(section.key)\">\n <he-svg-icon [name]=\"isOpen(section.key) ? 'chevron-down' : 'chevron-right'\" size=\"16\" />\n <span>{{ section.label }}</span>\n </a>\n\n @if (isOpen(section.key)) {\n <div class=\"log-section-body is-pb-2\">\n @switch (section.key) {\n @case ('logs') {\n <he-node-logs-models-logs [logs]=\"model().logs\" [renderRaw]=\"false\" />\n }\n @case ('raw') {\n <div class=\"is-flex is-justify-content-flex-end is-mb-1\">\n <he-clipboard clipboardClass=\"is-size-7 is-p-1\" [value]=\"rawJson()\" [hideText]=\"true\" />\n </div>\n <pre class=\"raw-log\">{{ rawJson() }}</pre>\n }\n @case ('contributions') {\n <he-node-logs-models-contributions [node]=\"node()\" [nodeKey]=\"nodeKey()\" [model]=\"model()\" />\n }\n }\n </div>\n }\n }\n </div>\n}\n", styles: [".log-section{border-color:#fff3!important}.log-section-header{cursor:pointer;padding:.5rem 0}.log-section-header ::ng-deep .formula-block{margin-bottom:0;border-bottom:none}.raw-log{max-height:300px;overflow:auto;white-space:pre;font-size:.75rem;background:#00000059;color:inherit;padding:.5rem;border-radius:3px}\n"], dependencies: [{ kind: "component", type: HESvgIconComponent, selector: "he-svg-icon", inputs: ["name", "size", "animation"] }, { kind: "component", type: ClipboardComponent, selector: "he-clipboard", inputs: ["icon", "value", "disabled", "hideText", "hideIcon", "size", "clipboardClass", "tooltipPlacement"] }, { kind: "component", type: NodeLogsModelsFormulaComponent, selector: "he-node-logs-models-formula", inputs: ["model", "logs", "value", "node"] }, { kind: "component", type: NodeLogsModelsLogsComponent, selector: "he-node-logs-models-logs", inputs: ["logs", "renderRaw"] }, { kind: "component", type: NodeLogsModelsContributionsComponent, selector: "he-node-logs-models-contributions", inputs: ["node", "nodeKey", "model"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9464
9543
  }
9465
9544
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NodeLogsModelsDetailsComponent, decorators: [{
9466
9545
  type: Component$1,
@@ -9470,7 +9549,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
9470
9549
  NodeLogsModelsFormulaComponent,
9471
9550
  NodeLogsModelsLogsComponent,
9472
9551
  NodeLogsModelsContributionsComponent
9473
- ], template: "@for (section of sections(); track section.key; let last = $last) {\n <div class=\"log-section\" [class.has-border-bottom]=\"!last || isOpen(section.key)\">\n @if (section.key === 'formula' && isOpen('formula')) {\n <!-- the formula component renders its own \"Formula BETA\" title + Raw/Substituted switch on one\n line, so keep the toggle chevron inline with it rather than adding a duplicate header -->\n <div class=\"log-section-header is-flex is-align-items-flex-start is-gap-4\">\n <a class=\"has-text-white\" (click)=\"toggle('formula')\">\n <he-svg-icon name=\"chevron-down\" size=\"16\" />\n </a>\n <div class=\"is-flex-grow-1\">\n <he-node-logs-models-formula [model]=\"model().model\" [logs]=\"model().logs\" [value]=\"value()\" />\n </div>\n </div>\n } @else {\n <a\n class=\"log-section-header is-flex is-align-items-center is-gap-4 has-text-white is-size-8 is-uppercase has-text-weight-semibold\"\n (click)=\"toggle(section.key)\">\n <he-svg-icon [name]=\"isOpen(section.key) ? 'chevron-down' : 'chevron-right'\" size=\"16\" />\n <span>{{ section.label }}</span>\n </a>\n\n @if (isOpen(section.key)) {\n <div class=\"log-section-body is-pb-2\">\n @switch (section.key) {\n @case ('logs') {\n <he-node-logs-models-logs [logs]=\"model().logs\" [renderRaw]=\"false\" />\n }\n @case ('raw') {\n <div class=\"is-flex is-justify-content-flex-end is-mb-1\">\n <he-clipboard clipboardClass=\"is-size-7 is-p-1\" [value]=\"rawJson()\" [hideText]=\"true\" />\n </div>\n <pre class=\"raw-log\">{{ rawJson() }}</pre>\n }\n @case ('contributions') {\n <he-node-logs-models-contributions [node]=\"node()\" [nodeKey]=\"nodeKey()\" [model]=\"model()\" />\n }\n }\n </div>\n }\n }\n </div>\n}\n", styles: [".log-section{border-color:#fff3!important}.log-section-header{cursor:pointer;padding:.5rem 0}.log-section-header ::ng-deep .formula-block{margin-bottom:0;border-bottom:none}.raw-log{max-height:300px;overflow:auto;white-space:pre;font-size:.75rem;background:#00000059;color:inherit;padding:.5rem;border-radius:3px}\n"] }]
9552
+ ], template: "@for (section of sections(); track section.key; let last = $last) {\n <div class=\"log-section\" [class.has-border-bottom]=\"!last || isOpen(section.key)\">\n @if (section.key === 'formula' && isOpen('formula')) {\n <!-- the formula component renders its own \"Formula BETA\" title + Raw/Substituted switch on one\n line, so keep the toggle chevron inline with it rather than adding a duplicate header -->\n <div class=\"log-section-header is-flex is-align-items-flex-start is-gap-4\">\n <a class=\"has-text-white\" (click)=\"toggle('formula')\">\n <he-svg-icon name=\"chevron-down\" size=\"16\" />\n </a>\n <div class=\"is-flex-grow-1\">\n <he-node-logs-models-formula\n [model]=\"model().model\"\n [logs]=\"model().logs\"\n [value]=\"value()\"\n [node]=\"node()\" />\n </div>\n </div>\n } @else {\n <a\n class=\"log-section-header is-flex is-align-items-center is-gap-4 has-text-white is-size-8 is-uppercase has-text-weight-semibold\"\n (click)=\"toggle(section.key)\">\n <he-svg-icon [name]=\"isOpen(section.key) ? 'chevron-down' : 'chevron-right'\" size=\"16\" />\n <span>{{ section.label }}</span>\n </a>\n\n @if (isOpen(section.key)) {\n <div class=\"log-section-body is-pb-2\">\n @switch (section.key) {\n @case ('logs') {\n <he-node-logs-models-logs [logs]=\"model().logs\" [renderRaw]=\"false\" />\n }\n @case ('raw') {\n <div class=\"is-flex is-justify-content-flex-end is-mb-1\">\n <he-clipboard clipboardClass=\"is-size-7 is-p-1\" [value]=\"rawJson()\" [hideText]=\"true\" />\n </div>\n <pre class=\"raw-log\">{{ rawJson() }}</pre>\n }\n @case ('contributions') {\n <he-node-logs-models-contributions [node]=\"node()\" [nodeKey]=\"nodeKey()\" [model]=\"model()\" />\n }\n }\n </div>\n }\n }\n </div>\n}\n", styles: [".log-section{border-color:#fff3!important}.log-section-header{cursor:pointer;padding:.5rem 0}.log-section-header ::ng-deep .formula-block{margin-bottom:0;border-bottom:none}.raw-log{max-height:300px;overflow:auto;white-space:pre;font-size:.75rem;background:#00000059;color:inherit;padding:.5rem;border-radius:3px}\n"] }]
9474
9553
  }], propDecorators: { model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], nodeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeKey", required: false }] }], hasContributions: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasContributions", required: false }] }] } });
9475
9554
 
9476
9555
  /**
@@ -14234,7 +14313,7 @@ ${JSON.stringify(error)}
14234
14313
 
14235
14314
  ## Details
14236
14315
 
14237
- * Path: [open](${window.location.href})
14316
+ * Path: [open](${typeof window === 'undefined' ? baseUrl() : window.location.href})
14238
14317
  * Stage: \`${fileStatus?.replace('Done', '')?.replace('Error', '')}\`
14239
14318
 
14240
14319
  /label ~"Type::bug"
@@ -14823,7 +14902,7 @@ const mergedNodes = (selection, { tooltipOperator }) => {
14823
14902
  .attr('stroke-width', '2')
14824
14903
  .attr('rx', '3px')
14825
14904
  .attr('ry', '3px')
14826
- .style('user-select', window.innerWidth < 768 ? 'none' : null);
14905
+ .style('user-select', typeof window !== 'undefined' && window.innerWidth < 768 ? 'none' : null);
14827
14906
  selection.filter(d => !d.data.group && d.data.id !== nonLCAIndicatorsId).call(addTooltip, { tooltipOperator });
14828
14907
  selection
14829
14908
  .selectAll('.node-label')