tsichart-core 2.0.0 → 2.1.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.
- package/dist/index.d.ts +1254 -5
- package/dist/index.js +11676 -406
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11661 -390
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1075 -227
- package/dist/index.umd.js.map +1 -1
- package/dist/styles/index.css +9151 -7048
- package/dist/styles/index.css.map +1 -1
- package/package.json +1 -1
package/dist/index.umd.js
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TsiClient = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
+
function _mergeNamespaces(n, m) {
|
|
8
|
+
m.forEach(function (e) {
|
|
9
|
+
e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default' && !(k in n)) {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
return Object.freeze(n);
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
function ascending$3(a, b) {
|
|
8
23
|
return a == null || b == null ? NaN : a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
|
|
9
24
|
}
|
|
@@ -29015,7 +29030,7 @@
|
|
|
29015
29030
|
swimLaneLabelHeightPadding: 8,
|
|
29016
29031
|
labelLeftPadding: 28
|
|
29017
29032
|
};
|
|
29018
|
-
const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '
|
|
29033
|
+
const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '}', ']', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
|
|
29019
29034
|
const NONNUMERICTOPMARGIN = 8;
|
|
29020
29035
|
const LINECHARTTOPPADDING = 16;
|
|
29021
29036
|
const GRIDCONTAINERCLASS = 'tsi-gridContainer';
|
|
@@ -30451,35 +30466,41 @@
|
|
|
30451
30466
|
}
|
|
30452
30467
|
}
|
|
30453
30468
|
|
|
30454
|
-
|
|
30455
|
-
|
|
30469
|
+
/**
|
|
30470
|
+
* Constants for Legend component layout and behavior
|
|
30471
|
+
*/
|
|
30472
|
+
const LEGEND_CONSTANTS = {
|
|
30473
|
+
/** Height in pixels for each numeric split-by item (includes type selector dropdown) */
|
|
30474
|
+
NUMERIC_SPLITBY_HEIGHT: 44,
|
|
30475
|
+
/** Height in pixels for each non-numeric (categorical/events) split-by item */
|
|
30476
|
+
NON_NUMERIC_SPLITBY_HEIGHT: 24,
|
|
30477
|
+
/** Height in pixels for the series name label header */
|
|
30478
|
+
NAME_LABEL_HEIGHT: 24,
|
|
30479
|
+
/** Buffer distance in pixels from scroll edge before triggering "load more" */
|
|
30480
|
+
SCROLL_BUFFER: 40,
|
|
30481
|
+
/** Number of split-by items to load per batch when paginating */
|
|
30482
|
+
BATCH_SIZE: 20,
|
|
30483
|
+
/** Minimum height in pixels for aggregate container */
|
|
30484
|
+
MIN_AGGREGATE_HEIGHT: 201,
|
|
30485
|
+
/** Minimum width in pixels for each series label in compact mode */
|
|
30486
|
+
MIN_SERIES_WIDTH: 124,
|
|
30487
|
+
};
|
|
30456
30488
|
class Legend extends Component {
|
|
30457
30489
|
constructor(drawChart, renderTarget, legendWidth) {
|
|
30458
30490
|
super(renderTarget);
|
|
30459
30491
|
this.renderSplitBys = (aggKey, aggSelection, dataType, noSplitBys) => {
|
|
30460
|
-
|
|
30461
|
-
|
|
30462
|
-
var firstSplitByType = firstSplitBy ? firstSplitBy.visibleType : null;
|
|
30463
|
-
Object.keys(this.chartComponentData.displayState[aggKey].splitBys).reduce((isSame, curr) => {
|
|
30464
|
-
return (firstSplitByType == this.chartComponentData.displayState[aggKey].splitBys[curr].visibleType) && isSame;
|
|
30465
|
-
}, true);
|
|
30466
|
-
let showMoreSplitBys = () => {
|
|
30467
|
-
const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
|
|
30468
|
-
this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
|
|
30469
|
-
if (oldShownSplitBys != this.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
30470
|
-
this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
30471
|
-
}
|
|
30472
|
-
};
|
|
30492
|
+
const splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
|
|
30493
|
+
const showMoreSplitBys = () => this.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
|
|
30473
30494
|
let splitByContainer = aggSelection.selectAll(".tsi-splitByContainer").data([aggKey]);
|
|
30474
|
-
|
|
30495
|
+
const splitByContainerEntered = splitByContainer.enter().append("div")
|
|
30475
30496
|
.merge(splitByContainer)
|
|
30476
30497
|
.classed("tsi-splitByContainer", true);
|
|
30477
|
-
|
|
30498
|
+
const splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
|
|
30478
30499
|
.data(splitByLabelData.slice(0, this.chartComponentData.displayState[aggKey].shownSplitBys), function (d) {
|
|
30479
30500
|
return d;
|
|
30480
30501
|
});
|
|
30481
|
-
|
|
30482
|
-
|
|
30502
|
+
const self = this;
|
|
30503
|
+
const splitByLabelsEntered = splitByLabels
|
|
30483
30504
|
.enter()
|
|
30484
30505
|
.append("div")
|
|
30485
30506
|
.merge(splitByLabels)
|
|
@@ -30493,135 +30514,60 @@
|
|
|
30493
30514
|
}
|
|
30494
30515
|
})
|
|
30495
30516
|
.on("click", function (event, splitBy) {
|
|
30496
|
-
|
|
30497
|
-
self.toggleSplitByVisible(aggKey, splitBy);
|
|
30498
|
-
}
|
|
30499
|
-
else {
|
|
30500
|
-
self.toggleSticky(aggKey, splitBy);
|
|
30501
|
-
}
|
|
30502
|
-
self.drawChart();
|
|
30517
|
+
self.handleSplitByClick(aggKey, splitBy);
|
|
30503
30518
|
})
|
|
30504
30519
|
.on("mouseover", function (event, splitBy) {
|
|
30505
30520
|
event.stopPropagation();
|
|
30506
|
-
self.
|
|
30521
|
+
self.handleSplitByMouseOver(aggKey, splitBy);
|
|
30507
30522
|
})
|
|
30508
30523
|
.on("mouseout", function (event) {
|
|
30509
30524
|
event.stopPropagation();
|
|
30510
|
-
self.
|
|
30511
|
-
.attr("stroke-opacity", 1)
|
|
30512
|
-
.attr("fill-opacity", 1);
|
|
30513
|
-
self.labelMouseout(self.svgSelection, aggKey);
|
|
30525
|
+
self.handleSplitByMouseOut(aggKey);
|
|
30514
30526
|
})
|
|
30515
30527
|
.attr("class", (splitBy, i) => {
|
|
30516
|
-
|
|
30517
|
-
|
|
30518
|
-
return `tsi-splitByLabel
|
|
30528
|
+
const compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
|
|
30529
|
+
const shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
|
|
30530
|
+
return `tsi-splitByLabel ${compact} ${shown}`;
|
|
30519
30531
|
})
|
|
30520
|
-
.classed("stickied", (splitBy, i) =>
|
|
30521
|
-
|
|
30522
|
-
return aggKey == self.chartComponentData.stickiedKey.aggregateKey && splitBy == self.chartComponentData.stickiedKey.splitBy;
|
|
30523
|
-
}
|
|
30524
|
-
});
|
|
30525
|
-
var colors = Utils.createSplitByColors(self.chartComponentData.displayState, aggKey, self.chartOptions.keepSplitByColor);
|
|
30532
|
+
.classed("stickied", (splitBy, i) => self.isStickied(aggKey, splitBy));
|
|
30533
|
+
// Use helper methods to render each split-by element
|
|
30526
30534
|
splitByLabelsEntered.each(function (splitBy, j) {
|
|
30527
|
-
|
|
30535
|
+
const selection = select(this);
|
|
30536
|
+
// Add color key (conditionally based on data type and legend state)
|
|
30528
30537
|
if (dataType === DataTypes.Numeric || noSplitBys || self.legendState === 'compact') {
|
|
30529
|
-
|
|
30530
|
-
|
|
30531
|
-
.append("div")
|
|
30532
|
-
.attr("class", 'tsi-colorKey')
|
|
30533
|
-
.merge(colorKey);
|
|
30534
|
-
if (dataType === DataTypes.Numeric) {
|
|
30535
|
-
colorKeyEntered.style('background-color', (d) => {
|
|
30536
|
-
return d;
|
|
30537
|
-
});
|
|
30538
|
-
}
|
|
30539
|
-
else {
|
|
30540
|
-
self.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
|
|
30541
|
-
}
|
|
30542
|
-
select(this).classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && this.legendState !== 'compact');
|
|
30543
|
-
colorKey.exit().remove();
|
|
30538
|
+
self.addColorKey(selection, aggKey, splitBy, dataType);
|
|
30539
|
+
selection.classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && self.legendState !== 'compact');
|
|
30544
30540
|
}
|
|
30545
30541
|
else {
|
|
30546
|
-
|
|
30547
|
-
}
|
|
30548
|
-
if (select(this).select('.tsi-eyeIcon').empty()) {
|
|
30549
|
-
select(this).append("button")
|
|
30550
|
-
.attr("class", "tsi-eyeIcon")
|
|
30551
|
-
.attr('aria-label', () => {
|
|
30552
|
-
let showOrHide = self.chartComponentData.displayState[aggKey].splitBys[splitBy].visible ? self.getString('hide series') : self.getString('show series');
|
|
30553
|
-
return `${showOrHide} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`;
|
|
30554
|
-
})
|
|
30555
|
-
.attr('title', () => self.getString('Show/Hide values'))
|
|
30556
|
-
.on("click", function (event) {
|
|
30557
|
-
event.stopPropagation();
|
|
30558
|
-
self.toggleSplitByVisible(aggKey, splitBy);
|
|
30559
|
-
select(this)
|
|
30560
|
-
.classed("shown", Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy));
|
|
30561
|
-
self.drawChart();
|
|
30562
|
-
});
|
|
30563
|
-
}
|
|
30564
|
-
if (select(this).select('.tsi-seriesName').empty()) {
|
|
30565
|
-
let seriesName = select(this)
|
|
30566
|
-
.append('div')
|
|
30567
|
-
.attr('class', 'tsi-seriesName');
|
|
30568
|
-
Utils.appendFormattedElementsFromString(seriesName, noSplitBys ? (self.chartComponentData.displayState[aggKey].name) : splitBy);
|
|
30542
|
+
selection.selectAll('.tsi-colorKey').remove();
|
|
30569
30543
|
}
|
|
30544
|
+
// Add eye icon
|
|
30545
|
+
self.addEyeIcon(selection, aggKey, splitBy);
|
|
30546
|
+
// Add series name
|
|
30547
|
+
self.addSeriesName(selection, aggKey, splitBy);
|
|
30548
|
+
// Add series type selection for numeric data
|
|
30570
30549
|
if (dataType === DataTypes.Numeric) {
|
|
30571
|
-
|
|
30572
|
-
select(this).append("select")
|
|
30573
|
-
.attr('aria-label', `${self.getString("Series type selection for")} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`)
|
|
30574
|
-
.attr('class', 'tsi-seriesTypeSelection')
|
|
30575
|
-
.on("change", function (data) {
|
|
30576
|
-
var seriesType = select(this).property("value");
|
|
30577
|
-
self.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
|
|
30578
|
-
self.drawChart();
|
|
30579
|
-
})
|
|
30580
|
-
.on("click", (event) => {
|
|
30581
|
-
event.stopPropagation();
|
|
30582
|
-
});
|
|
30583
|
-
}
|
|
30584
|
-
select(this).select('.tsi-seriesTypeSelection')
|
|
30585
|
-
.each(function (d) {
|
|
30586
|
-
var typeLabels = select(this).selectAll('option')
|
|
30587
|
-
.data(data => self.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map((type) => {
|
|
30588
|
-
return {
|
|
30589
|
-
type: type,
|
|
30590
|
-
aggKey: aggKey,
|
|
30591
|
-
splitBy: splitBy,
|
|
30592
|
-
visibleMeasure: Utils.getAgVisibleMeasure(self.chartComponentData.displayState, aggKey, splitBy)
|
|
30593
|
-
};
|
|
30594
|
-
}));
|
|
30595
|
-
typeLabels
|
|
30596
|
-
.enter()
|
|
30597
|
-
.append("option")
|
|
30598
|
-
.attr("class", "seriesTypeLabel")
|
|
30599
|
-
.merge(typeLabels)
|
|
30600
|
-
.property("selected", (data) => {
|
|
30601
|
-
return ((data.type == Utils.getAgVisibleMeasure(self.chartComponentData.displayState, data.aggKey, data.splitBy)) ?
|
|
30602
|
-
" selected" : "");
|
|
30603
|
-
})
|
|
30604
|
-
.text((data) => data.type);
|
|
30605
|
-
typeLabels.exit().remove();
|
|
30606
|
-
});
|
|
30550
|
+
self.addSeriesTypeSelection(selection, aggKey, splitBy);
|
|
30607
30551
|
}
|
|
30608
30552
|
else {
|
|
30609
|
-
|
|
30553
|
+
selection.selectAll('.tsi-seriesTypeSelection').remove();
|
|
30610
30554
|
}
|
|
30611
30555
|
});
|
|
30612
30556
|
splitByLabels.exit().remove();
|
|
30613
|
-
|
|
30557
|
+
// Show more button
|
|
30558
|
+
const shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
|
|
30614
30559
|
splitByContainerEntered.selectAll('.tsi-legendShowMore').remove();
|
|
30615
30560
|
if (this.legendState === 'shown' && shouldShowMore) {
|
|
30616
30561
|
splitByContainerEntered.append('button')
|
|
30617
30562
|
.text(this.getString('Show more'))
|
|
30618
30563
|
.attr('class', 'tsi-legendShowMore')
|
|
30619
|
-
.style('display',
|
|
30564
|
+
.style('display', 'block')
|
|
30620
30565
|
.on('click', showMoreSplitBys);
|
|
30621
30566
|
}
|
|
30567
|
+
// Scroll handler for infinite scrolling
|
|
30622
30568
|
splitByContainerEntered.on("scroll", function () {
|
|
30623
30569
|
if (self.chartOptions.legend === 'shown') {
|
|
30624
|
-
if (this.scrollTop + this.clientHeight +
|
|
30570
|
+
if (this.scrollTop + this.clientHeight + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollHeight) {
|
|
30625
30571
|
showMoreSplitBys();
|
|
30626
30572
|
}
|
|
30627
30573
|
}
|
|
@@ -30646,10 +30592,125 @@
|
|
|
30646
30592
|
};
|
|
30647
30593
|
this.drawChart = drawChart;
|
|
30648
30594
|
this.legendWidth = legendWidth;
|
|
30649
|
-
this.legendElement = select(renderTarget)
|
|
30595
|
+
this.legendElement = select(renderTarget)
|
|
30596
|
+
.insert("div", ":first-child")
|
|
30650
30597
|
.attr("class", "tsi-legend")
|
|
30651
|
-
.style("left", "0px")
|
|
30652
|
-
|
|
30598
|
+
.style("left", "0px");
|
|
30599
|
+
// Note: width is set conditionally in draw() based on legendState
|
|
30600
|
+
// to allow CSS to control width in compact mode
|
|
30601
|
+
}
|
|
30602
|
+
getHeightPerSplitBy(aggKey) {
|
|
30603
|
+
const dataType = this.chartComponentData.displayState[aggKey].dataType;
|
|
30604
|
+
return dataType === DataTypes.Numeric
|
|
30605
|
+
? LEGEND_CONSTANTS.NUMERIC_SPLITBY_HEIGHT
|
|
30606
|
+
: LEGEND_CONSTANTS.NON_NUMERIC_SPLITBY_HEIGHT;
|
|
30607
|
+
}
|
|
30608
|
+
addColorKey(selection, aggKey, splitBy, dataType) {
|
|
30609
|
+
const colors = Utils.createSplitByColors(this.chartComponentData.displayState, aggKey, this.chartOptions.keepSplitByColor);
|
|
30610
|
+
const splitByKeys = Object.keys(this.chartComponentData.timeArrays[aggKey]);
|
|
30611
|
+
const splitByIndex = splitByKeys.indexOf(splitBy);
|
|
30612
|
+
const color = this.chartComponentData.isFromHeatmap
|
|
30613
|
+
? this.chartComponentData.displayState[aggKey].color
|
|
30614
|
+
: colors[splitByIndex];
|
|
30615
|
+
const colorKey = selection.selectAll('.tsi-colorKey').data([color]);
|
|
30616
|
+
const colorKeyEntered = colorKey.enter()
|
|
30617
|
+
.append('div')
|
|
30618
|
+
.attr('class', 'tsi-colorKey')
|
|
30619
|
+
.merge(colorKey);
|
|
30620
|
+
if (dataType === DataTypes.Numeric) {
|
|
30621
|
+
colorKeyEntered.style('background-color', d => d);
|
|
30622
|
+
}
|
|
30623
|
+
else {
|
|
30624
|
+
this.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
|
|
30625
|
+
}
|
|
30626
|
+
colorKey.exit().remove();
|
|
30627
|
+
}
|
|
30628
|
+
addEyeIcon(selection, aggKey, splitBy) {
|
|
30629
|
+
if (selection.select('.tsi-eyeIcon').empty()) {
|
|
30630
|
+
selection.append('button')
|
|
30631
|
+
.attr('class', 'tsi-eyeIcon')
|
|
30632
|
+
.attr('aria-label', () => {
|
|
30633
|
+
const showOrHide = this.chartComponentData.displayState[aggKey].splitBys[splitBy].visible
|
|
30634
|
+
? this.getString('hide series')
|
|
30635
|
+
: this.getString('show series');
|
|
30636
|
+
return `${showOrHide} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`;
|
|
30637
|
+
})
|
|
30638
|
+
.attr('title', () => this.getString('Show/Hide values'))
|
|
30639
|
+
.on('click', (event) => {
|
|
30640
|
+
event.stopPropagation();
|
|
30641
|
+
this.toggleSplitByVisible(aggKey, splitBy);
|
|
30642
|
+
this.drawChart();
|
|
30643
|
+
});
|
|
30644
|
+
}
|
|
30645
|
+
selection.select('.tsi-eyeIcon')
|
|
30646
|
+
.classed('shown', Utils.getAgVisible(this.chartComponentData.displayState, aggKey, splitBy));
|
|
30647
|
+
}
|
|
30648
|
+
addSeriesName(selection, aggKey, splitBy) {
|
|
30649
|
+
if (selection.select('.tsi-seriesName').empty()) {
|
|
30650
|
+
const seriesName = selection.append('div')
|
|
30651
|
+
.attr('class', 'tsi-seriesName');
|
|
30652
|
+
const noSplitBys = Object.keys(this.chartComponentData.timeArrays[aggKey]).length === 1
|
|
30653
|
+
&& Object.keys(this.chartComponentData.timeArrays[aggKey])[0] === '';
|
|
30654
|
+
const displayText = noSplitBys
|
|
30655
|
+
? this.chartComponentData.displayState[aggKey].name
|
|
30656
|
+
: splitBy;
|
|
30657
|
+
Utils.appendFormattedElementsFromString(seriesName, displayText);
|
|
30658
|
+
}
|
|
30659
|
+
}
|
|
30660
|
+
addSeriesTypeSelection(selection, aggKey, splitBy) {
|
|
30661
|
+
if (selection.select('.tsi-seriesTypeSelection').empty()) {
|
|
30662
|
+
selection.append('select')
|
|
30663
|
+
.attr('aria-label', `${this.getString('Series type selection for')} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`)
|
|
30664
|
+
.attr('class', 'tsi-seriesTypeSelection')
|
|
30665
|
+
.on('change', (event) => {
|
|
30666
|
+
const seriesType = select(event.target).property('value');
|
|
30667
|
+
this.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
|
|
30668
|
+
this.drawChart();
|
|
30669
|
+
})
|
|
30670
|
+
.on('click', (event) => {
|
|
30671
|
+
event.stopPropagation();
|
|
30672
|
+
});
|
|
30673
|
+
}
|
|
30674
|
+
selection.select('.tsi-seriesTypeSelection')
|
|
30675
|
+
.each((d, i, nodes) => {
|
|
30676
|
+
const typeLabels = select(nodes[i])
|
|
30677
|
+
.selectAll('option')
|
|
30678
|
+
.data(this.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map(type => ({
|
|
30679
|
+
type,
|
|
30680
|
+
aggKey,
|
|
30681
|
+
splitBy,
|
|
30682
|
+
visibleMeasure: Utils.getAgVisibleMeasure(this.chartComponentData.displayState, aggKey, splitBy)
|
|
30683
|
+
})));
|
|
30684
|
+
typeLabels.enter()
|
|
30685
|
+
.append('option')
|
|
30686
|
+
.attr('class', 'seriesTypeLabel')
|
|
30687
|
+
.merge(typeLabels)
|
|
30688
|
+
.property('selected', (data) => data.type === Utils.getAgVisibleMeasure(this.chartComponentData.displayState, data.aggKey, data.splitBy))
|
|
30689
|
+
.text((data) => data.type);
|
|
30690
|
+
typeLabels.exit().remove();
|
|
30691
|
+
});
|
|
30692
|
+
}
|
|
30693
|
+
handleSplitByClick(aggKey, splitBy) {
|
|
30694
|
+
if (this.legendState === 'compact') {
|
|
30695
|
+
this.toggleSplitByVisible(aggKey, splitBy);
|
|
30696
|
+
}
|
|
30697
|
+
else {
|
|
30698
|
+
this.toggleSticky(aggKey, splitBy);
|
|
30699
|
+
}
|
|
30700
|
+
this.drawChart();
|
|
30701
|
+
}
|
|
30702
|
+
handleSplitByMouseOver(aggKey, splitBy) {
|
|
30703
|
+
this.labelMouseover(aggKey, splitBy);
|
|
30704
|
+
}
|
|
30705
|
+
handleSplitByMouseOut(aggKey) {
|
|
30706
|
+
this.svgSelection.selectAll(".tsi-valueElement")
|
|
30707
|
+
.attr("stroke-opacity", 1)
|
|
30708
|
+
.attr("fill-opacity", 1);
|
|
30709
|
+
this.labelMouseout(this.svgSelection, aggKey);
|
|
30710
|
+
}
|
|
30711
|
+
isStickied(aggKey, splitBy) {
|
|
30712
|
+
const stickied = this.chartComponentData.stickiedKey;
|
|
30713
|
+
return stickied?.aggregateKey === aggKey && stickied?.splitBy === splitBy;
|
|
30653
30714
|
}
|
|
30654
30715
|
labelMouseoutWrapper(labelMouseout, svgSelection, event) {
|
|
30655
30716
|
return (svgSelection, aggKey) => {
|
|
@@ -30691,14 +30752,11 @@
|
|
|
30691
30752
|
return d == aggKey;
|
|
30692
30753
|
}).node();
|
|
30693
30754
|
var prospectiveScrollTop = Math.max((indexOfSplitBy - 1) * this.getHeightPerSplitBy(aggKey), 0);
|
|
30694
|
-
if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight -
|
|
30755
|
+
if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - LEGEND_CONSTANTS.SCROLL_BUFFER) || splitByNode.scrollTop > prospectiveScrollTop) {
|
|
30695
30756
|
splitByNode.scrollTop = prospectiveScrollTop;
|
|
30696
30757
|
}
|
|
30697
30758
|
}
|
|
30698
30759
|
}
|
|
30699
|
-
getHeightPerSplitBy(aggKey) {
|
|
30700
|
-
return (this.chartComponentData.displayState[aggKey].dataType === DataTypes.Numeric ? NUMERICSPLITBYHEIGHT : NONNUMERICSPLITBYHEIGHT);
|
|
30701
|
-
}
|
|
30702
30760
|
createGradient(gradientKey, svg, values) {
|
|
30703
30761
|
let gradient = svg.append('defs').append('linearGradient')
|
|
30704
30762
|
.attr('id', gradientKey).attr('x1', '0%').attr('x2', '0%').attr('y1', '0%').attr('y2', '100%');
|
|
@@ -30717,10 +30775,6 @@
|
|
|
30717
30775
|
.attr("stop-opacity", 1);
|
|
30718
30776
|
});
|
|
30719
30777
|
}
|
|
30720
|
-
isNonNumeric(aggKey) {
|
|
30721
|
-
let dataType = this.chartComponentData.displayState[aggKey].dataType;
|
|
30722
|
-
return (dataType === DataTypes.Categorical || dataType === DataTypes.Events);
|
|
30723
|
-
}
|
|
30724
30778
|
createNonNumericColorKey(dataType, colorKey, aggKey) {
|
|
30725
30779
|
if (dataType === DataTypes.Categorical) {
|
|
30726
30780
|
this.createCategoricalColorKey(colorKey, aggKey);
|
|
@@ -30776,6 +30830,13 @@
|
|
|
30776
30830
|
rect.attr('fill', "url(#" + gradientKey + ")");
|
|
30777
30831
|
}
|
|
30778
30832
|
}
|
|
30833
|
+
handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys) {
|
|
30834
|
+
const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
|
|
30835
|
+
this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + LEGEND_CONSTANTS.BATCH_SIZE, splitByLabelData.length);
|
|
30836
|
+
if (oldShownSplitBys !== this.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
30837
|
+
this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
30838
|
+
}
|
|
30839
|
+
}
|
|
30779
30840
|
draw(legendState, chartComponentData, labelMouseover, svgSelection, options, labelMouseoutAction = null, stickySeriesAction = null, event) {
|
|
30780
30841
|
this.chartOptions.setOptions(options);
|
|
30781
30842
|
this.chartComponentData = chartComponentData;
|
|
@@ -30790,6 +30851,13 @@
|
|
|
30790
30851
|
legend.style('visibility', this.legendState != 'hidden')
|
|
30791
30852
|
.classed('compact', this.legendState == 'compact')
|
|
30792
30853
|
.classed('hidden', this.legendState == 'hidden');
|
|
30854
|
+
// Set width conditionally - let CSS handle compact mode width
|
|
30855
|
+
if (this.legendState !== 'compact') {
|
|
30856
|
+
legend.style('width', `${this.legendWidth}px`);
|
|
30857
|
+
}
|
|
30858
|
+
else {
|
|
30859
|
+
legend.style('width', null); // Remove inline width style in compact mode
|
|
30860
|
+
}
|
|
30793
30861
|
let seriesNames = Object.keys(this.chartComponentData.displayState);
|
|
30794
30862
|
var seriesLabels = legend.selectAll(".tsi-seriesLabel")
|
|
30795
30863
|
.data(seriesNames, d => d);
|
|
@@ -30800,7 +30868,7 @@
|
|
|
30800
30868
|
return "tsi-seriesLabel " + (this.chartComponentData.displayState[d]["visible"] ? " shown" : "");
|
|
30801
30869
|
})
|
|
30802
30870
|
.style("min-width", () => {
|
|
30803
|
-
return Math.min(
|
|
30871
|
+
return Math.min(LEGEND_CONSTANTS.MIN_SERIES_WIDTH, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
|
|
30804
30872
|
})
|
|
30805
30873
|
.style("border-color", function (d, i) {
|
|
30806
30874
|
if (select(this).classed("shown"))
|
|
@@ -30808,9 +30876,8 @@
|
|
|
30808
30876
|
return "lightgray";
|
|
30809
30877
|
});
|
|
30810
30878
|
var self = this;
|
|
30811
|
-
const heightPerNameLabel = 25;
|
|
30812
30879
|
const usableLegendHeight = legend.node().clientHeight;
|
|
30813
|
-
var prospectiveAggregateHeight = Math.ceil(Math.max(
|
|
30880
|
+
var prospectiveAggregateHeight = Math.ceil(Math.max(LEGEND_CONSTANTS.MIN_AGGREGATE_HEIGHT, (usableLegendHeight / seriesLabelsEntered.size())));
|
|
30814
30881
|
var contentHeight = 0;
|
|
30815
30882
|
seriesLabelsEntered.each(function (aggKey, i) {
|
|
30816
30883
|
let heightPerSplitBy = self.getHeightPerSplitBy(aggKey);
|
|
@@ -30866,12 +30933,12 @@
|
|
|
30866
30933
|
seriesNameLabel.exit().remove();
|
|
30867
30934
|
var splitByContainerHeight;
|
|
30868
30935
|
if (splitByLabelData.length > (prospectiveAggregateHeight / heightPerSplitBy)) {
|
|
30869
|
-
splitByContainerHeight = prospectiveAggregateHeight -
|
|
30870
|
-
contentHeight += splitByContainerHeight +
|
|
30936
|
+
splitByContainerHeight = prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
30937
|
+
contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
30871
30938
|
}
|
|
30872
30939
|
else if (splitByLabelData.length > 1 || (splitByLabelData.length === 1 && splitByLabelData[0] !== "")) {
|
|
30873
|
-
splitByContainerHeight = splitByLabelData.length * heightPerSplitBy +
|
|
30874
|
-
contentHeight += splitByContainerHeight +
|
|
30940
|
+
splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
30941
|
+
contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
|
|
30875
30942
|
}
|
|
30876
30943
|
else {
|
|
30877
30944
|
splitByContainerHeight = heightPerSplitBy;
|
|
@@ -30884,43 +30951,28 @@
|
|
|
30884
30951
|
select(this).style("height", "unset");
|
|
30885
30952
|
}
|
|
30886
30953
|
var splitByContainer = select(this).selectAll(".tsi-splitByContainer").data([aggKey]);
|
|
30887
|
-
|
|
30954
|
+
splitByContainer.enter().append("div")
|
|
30888
30955
|
.merge(splitByContainer)
|
|
30889
30956
|
.classed("tsi-splitByContainer", true);
|
|
30890
30957
|
let aggSelection = select(this);
|
|
30891
30958
|
self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
30892
|
-
|
|
30893
|
-
if (self.chartOptions.legend == "shown") {
|
|
30894
|
-
if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
|
|
30895
|
-
const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
|
|
30896
|
-
self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
|
|
30897
|
-
if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
30898
|
-
self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
|
|
30899
|
-
}
|
|
30900
|
-
}
|
|
30901
|
-
}
|
|
30902
|
-
});
|
|
30959
|
+
// Compact mode horizontal scroll handler
|
|
30903
30960
|
select(this).on('scroll', function () {
|
|
30904
30961
|
if (self.chartOptions.legend == "compact") {
|
|
30905
|
-
if (this.scrollLeft + this.clientWidth +
|
|
30906
|
-
|
|
30907
|
-
self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
|
|
30908
|
-
if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
|
|
30909
|
-
this.renderSplitBys(dataType);
|
|
30910
|
-
}
|
|
30962
|
+
if (this.scrollLeft + this.clientWidth + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollWidth) {
|
|
30963
|
+
self.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
|
|
30911
30964
|
}
|
|
30912
30965
|
}
|
|
30913
30966
|
});
|
|
30914
30967
|
splitByContainer.exit().remove();
|
|
30915
30968
|
});
|
|
30916
30969
|
if (this.chartOptions.legend == 'shown') {
|
|
30917
|
-
legend.node().clientHeight;
|
|
30918
30970
|
//minSplitBysForFlexGrow: the minimum number of split bys for flex-grow to be triggered
|
|
30919
30971
|
if (contentHeight < usableLegendHeight) {
|
|
30920
30972
|
this.legendElement.classed("tsi-flexLegend", true);
|
|
30921
30973
|
seriesLabelsEntered.each(function (d) {
|
|
30922
30974
|
let heightPerSplitBy = self.getHeightPerSplitBy(d);
|
|
30923
|
-
var minSplitByForFlexGrow = (prospectiveAggregateHeight -
|
|
30975
|
+
var minSplitByForFlexGrow = (prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT) / heightPerSplitBy;
|
|
30924
30976
|
var splitBysCount = Object.keys(self.chartComponentData.displayState[String(select(this).data()[0])].splitBys).length;
|
|
30925
30977
|
if (splitBysCount > minSplitByForFlexGrow) {
|
|
30926
30978
|
select(this).style("flex-grow", 1);
|
|
@@ -30933,6 +30985,12 @@
|
|
|
30933
30985
|
}
|
|
30934
30986
|
seriesLabels.exit().remove();
|
|
30935
30987
|
}
|
|
30988
|
+
destroy() {
|
|
30989
|
+
this.legendElement.remove();
|
|
30990
|
+
// Note: Virtual list cleanup will be added when virtual scrolling is implemented
|
|
30991
|
+
// this.virtualLists.forEach(list => list.destroy());
|
|
30992
|
+
// this.virtualLists.clear();
|
|
30993
|
+
}
|
|
30936
30994
|
}
|
|
30937
30995
|
|
|
30938
30996
|
class ChartComponentData {
|
|
@@ -36749,6 +36807,8 @@
|
|
|
36749
36807
|
.append("text")
|
|
36750
36808
|
.attr("class", (d) => `tsi-swimLaneLabel-${lane} tsi-swimLaneLabel ${onClickPresentAndValid(d) ? 'tsi-boldOnHover' : ''}`)
|
|
36751
36809
|
.attr("role", "heading")
|
|
36810
|
+
.attr("aria-roledescription", this.getString("Swimlane label"))
|
|
36811
|
+
.attr("aria-label", d => d.label)
|
|
36752
36812
|
.attr("aria-level", "3")
|
|
36753
36813
|
.merge(label)
|
|
36754
36814
|
.style("text-anchor", "middle")
|
|
@@ -37290,19 +37350,19 @@
|
|
|
37290
37350
|
var momentExports = requireMoment();
|
|
37291
37351
|
var moment = /*@__PURE__*/getDefaultExportFromCjs(momentExports);
|
|
37292
37352
|
|
|
37293
|
-
var pikaday$
|
|
37353
|
+
var pikaday$2 = {exports: {}};
|
|
37294
37354
|
|
|
37295
37355
|
/*!
|
|
37296
37356
|
* Pikaday
|
|
37297
37357
|
*
|
|
37298
37358
|
* Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday
|
|
37299
37359
|
*/
|
|
37300
|
-
var pikaday = pikaday$
|
|
37360
|
+
var pikaday$1 = pikaday$2.exports;
|
|
37301
37361
|
|
|
37302
37362
|
var hasRequiredPikaday;
|
|
37303
37363
|
|
|
37304
37364
|
function requirePikaday () {
|
|
37305
|
-
if (hasRequiredPikaday) return pikaday$
|
|
37365
|
+
if (hasRequiredPikaday) return pikaday$2.exports;
|
|
37306
37366
|
hasRequiredPikaday = 1;
|
|
37307
37367
|
(function (module, exports) {
|
|
37308
37368
|
(function (root, factory) {
|
|
@@ -37317,7 +37377,7 @@
|
|
|
37317
37377
|
}(typeof self !== 'undefined' ? self :
|
|
37318
37378
|
typeof window !== 'undefined' ? window :
|
|
37319
37379
|
typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
|
|
37320
|
-
pikaday, function (moment) {
|
|
37380
|
+
pikaday$1, function (moment) {
|
|
37321
37381
|
|
|
37322
37382
|
/**
|
|
37323
37383
|
* feature detection and helper functions
|
|
@@ -38476,33 +38536,77 @@
|
|
|
38476
38536
|
|
|
38477
38537
|
return Pikaday;
|
|
38478
38538
|
}));
|
|
38479
|
-
} (pikaday$
|
|
38480
|
-
return pikaday$
|
|
38539
|
+
} (pikaday$2));
|
|
38540
|
+
return pikaday$2.exports;
|
|
38481
38541
|
}
|
|
38482
38542
|
|
|
38483
38543
|
var pikadayExports = /*@__PURE__*/ requirePikaday();
|
|
38484
|
-
var
|
|
38544
|
+
var pikaday = /*@__PURE__*/getDefaultExportFromCjs(pikadayExports);
|
|
38545
|
+
|
|
38546
|
+
var pikadayNamespace = /*#__PURE__*/_mergeNamespaces({
|
|
38547
|
+
__proto__: null,
|
|
38548
|
+
default: pikaday
|
|
38549
|
+
}, [pikadayExports]);
|
|
38485
38550
|
|
|
38486
38551
|
// Ensure moment is available globally for Pikaday
|
|
38487
38552
|
if (typeof window !== 'undefined') {
|
|
38488
38553
|
window.moment = moment;
|
|
38489
38554
|
}
|
|
38555
|
+
// Get Pikaday from the module or window
|
|
38556
|
+
// The UMD wrapper will execute and either:
|
|
38557
|
+
// 1. Return the constructor via module.exports (Rollup/CommonJS)
|
|
38558
|
+
// 2. Attach it to window.Pikaday (Browser/UMD)
|
|
38559
|
+
let Pikaday$1 = null;
|
|
38560
|
+
if (typeof window !== 'undefined') {
|
|
38561
|
+
// Check window first (UMD attached it there)
|
|
38562
|
+
if (window.Pikaday) {
|
|
38563
|
+
Pikaday$1 = window.Pikaday;
|
|
38564
|
+
}
|
|
38565
|
+
// Check if imported as default (CommonJS module.exports)
|
|
38566
|
+
else if (pikaday && typeof pikaday === 'function') {
|
|
38567
|
+
Pikaday$1 = pikaday;
|
|
38568
|
+
window.Pikaday = Pikaday$1;
|
|
38569
|
+
}
|
|
38570
|
+
// Check if it's the namespace itself (rare)
|
|
38571
|
+
else if (typeof pikadayNamespace === 'function') {
|
|
38572
|
+
Pikaday$1 = pikadayNamespace;
|
|
38573
|
+
window.Pikaday = Pikaday$1;
|
|
38574
|
+
}
|
|
38575
|
+
// Try any other property that might be the constructor
|
|
38576
|
+
else if (pikadayNamespace) {
|
|
38577
|
+
const possiblePikaday = Object.values(pikadayNamespace).find(val => typeof val === 'function');
|
|
38578
|
+
if (possiblePikaday) {
|
|
38579
|
+
Pikaday$1 = possiblePikaday;
|
|
38580
|
+
window.Pikaday = Pikaday$1;
|
|
38581
|
+
}
|
|
38582
|
+
}
|
|
38583
|
+
}
|
|
38490
38584
|
// Export a function to safely create Pikaday instances
|
|
38491
38585
|
function createPikaday(options) {
|
|
38492
38586
|
if (typeof window === 'undefined') {
|
|
38493
38587
|
console.warn('Pikaday requires a browser environment');
|
|
38494
38588
|
return null;
|
|
38495
38589
|
}
|
|
38496
|
-
|
|
38497
|
-
|
|
38590
|
+
// Try multiple sources for Pikaday
|
|
38591
|
+
const PikadayConstructor = Pikaday$1 || window.Pikaday;
|
|
38592
|
+
if (!PikadayConstructor) {
|
|
38498
38593
|
console.error('Pikaday not available. Make sure pikaday.js is loaded.');
|
|
38594
|
+
console.error('Failed to create Pikaday calendar. Check moment.js availability.');
|
|
38499
38595
|
return null;
|
|
38500
38596
|
}
|
|
38501
38597
|
if (!moment || !window.moment) {
|
|
38502
38598
|
console.error('Moment.js not available. Pikaday requires moment.js.');
|
|
38599
|
+
console.error('Failed to create Pikaday calendar. Check moment.js availability.');
|
|
38600
|
+
return null;
|
|
38601
|
+
}
|
|
38602
|
+
try {
|
|
38603
|
+
return new PikadayConstructor(options);
|
|
38604
|
+
}
|
|
38605
|
+
catch (error) {
|
|
38606
|
+
console.error('Failed to create Pikaday instance:', error);
|
|
38607
|
+
console.error('Failed to create Pikaday calendar. Check moment.js availability.');
|
|
38503
38608
|
return null;
|
|
38504
38609
|
}
|
|
38505
|
-
return new Pikaday(options);
|
|
38506
38610
|
}
|
|
38507
38611
|
|
|
38508
38612
|
class DateTimePicker extends ChartComponent {
|
|
@@ -39077,7 +39181,30 @@
|
|
|
39077
39181
|
this.pickerIsVisible = false;
|
|
39078
39182
|
}
|
|
39079
39183
|
buttonDateTimeFormat(millis) {
|
|
39080
|
-
|
|
39184
|
+
const date = new Date(millis);
|
|
39185
|
+
const locale = this.chartOptions.dateLocale || 'en-US';
|
|
39186
|
+
const is24Hour = this.chartOptions.is24HourTime !== false;
|
|
39187
|
+
const formatOptions = {
|
|
39188
|
+
year: 'numeric',
|
|
39189
|
+
month: '2-digit',
|
|
39190
|
+
day: '2-digit',
|
|
39191
|
+
hour: '2-digit',
|
|
39192
|
+
minute: '2-digit',
|
|
39193
|
+
second: '2-digit',
|
|
39194
|
+
hour12: !is24Hour
|
|
39195
|
+
};
|
|
39196
|
+
try {
|
|
39197
|
+
if (this.chartOptions.offset && this.chartOptions.offset !== 'Local') {
|
|
39198
|
+
formatOptions.timeZone = this.getTimezoneFromOffset(this.chartOptions.offset);
|
|
39199
|
+
}
|
|
39200
|
+
const baseFormat = date.toLocaleString(locale, formatOptions);
|
|
39201
|
+
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
39202
|
+
return `${baseFormat}.${milliseconds}`;
|
|
39203
|
+
}
|
|
39204
|
+
catch (error) {
|
|
39205
|
+
console.warn(`Failed to format date for locale ${locale}:`, error);
|
|
39206
|
+
return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
|
|
39207
|
+
}
|
|
39081
39208
|
}
|
|
39082
39209
|
render(chartOptions, minMillis, maxMillis, onSet = null) {
|
|
39083
39210
|
this.chartOptions.setOptions(chartOptions);
|
|
@@ -39097,11 +39224,22 @@
|
|
|
39097
39224
|
}
|
|
39098
39225
|
super.themify(select(this.renderTarget), this.chartOptions.theme);
|
|
39099
39226
|
}
|
|
39227
|
+
getTimezoneFromOffset(offset) {
|
|
39228
|
+
const timezoneMap = {
|
|
39229
|
+
'UTC': 'UTC',
|
|
39230
|
+
'EST': 'America/New_York',
|
|
39231
|
+
'PST': 'America/Los_Angeles',
|
|
39232
|
+
'CST': 'America/Chicago',
|
|
39233
|
+
'MST': 'America/Denver'
|
|
39234
|
+
};
|
|
39235
|
+
return timezoneMap[offset] || 'UTC';
|
|
39236
|
+
}
|
|
39100
39237
|
}
|
|
39101
39238
|
|
|
39102
39239
|
class DateTimeButtonRange extends DateTimeButton {
|
|
39103
39240
|
constructor(renderTarget) {
|
|
39104
39241
|
super(renderTarget);
|
|
39242
|
+
this.clickOutsideHandler = null;
|
|
39105
39243
|
}
|
|
39106
39244
|
setButtonText(fromMillis, toMillis, isRelative, quickTime) {
|
|
39107
39245
|
let fromString = this.buttonDateTimeFormat(fromMillis);
|
|
@@ -39121,10 +39259,38 @@
|
|
|
39121
39259
|
onClose() {
|
|
39122
39260
|
this.dateTimePickerContainer.style("display", "none");
|
|
39123
39261
|
this.dateTimeButton.node().focus();
|
|
39262
|
+
this.removeClickOutsideHandler();
|
|
39263
|
+
}
|
|
39264
|
+
removeClickOutsideHandler() {
|
|
39265
|
+
if (this.clickOutsideHandler) {
|
|
39266
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
39267
|
+
this.clickOutsideHandler = null;
|
|
39268
|
+
}
|
|
39269
|
+
}
|
|
39270
|
+
setupClickOutsideHandler() {
|
|
39271
|
+
// Remove any existing handler first
|
|
39272
|
+
this.removeClickOutsideHandler();
|
|
39273
|
+
// Add handler after a small delay to prevent the opening click from immediately closing the picker
|
|
39274
|
+
setTimeout(() => {
|
|
39275
|
+
this.clickOutsideHandler = (event) => {
|
|
39276
|
+
const pickerElement = this.dateTimePickerContainer.node();
|
|
39277
|
+
const buttonElement = this.dateTimeButton.node();
|
|
39278
|
+
const target = event.target;
|
|
39279
|
+
// Check if click is outside both the picker and the button
|
|
39280
|
+
if (pickerElement && buttonElement &&
|
|
39281
|
+
!pickerElement.contains(target) &&
|
|
39282
|
+
!buttonElement.contains(target)) {
|
|
39283
|
+
this.onClose();
|
|
39284
|
+
}
|
|
39285
|
+
};
|
|
39286
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
39287
|
+
}, 0);
|
|
39124
39288
|
}
|
|
39125
39289
|
render(chartOptions = {}, minMillis, maxMillis, fromMillis = null, toMillis = null, onSet = null, onCancel = null) {
|
|
39126
39290
|
super.render(chartOptions, minMillis, maxMillis, onSet);
|
|
39127
|
-
select(this.renderTarget)
|
|
39291
|
+
let container = select(this.renderTarget);
|
|
39292
|
+
container.classed('tsi-dateTimeContainerRange', true);
|
|
39293
|
+
container.style('position', 'relative');
|
|
39128
39294
|
this.fromMillis = fromMillis;
|
|
39129
39295
|
this.toMillis = toMillis;
|
|
39130
39296
|
this.onCancel = onCancel ? onCancel : () => { };
|
|
@@ -39150,6 +39316,7 @@
|
|
|
39150
39316
|
this.onClose();
|
|
39151
39317
|
this.onCancel();
|
|
39152
39318
|
});
|
|
39319
|
+
this.setupClickOutsideHandler();
|
|
39153
39320
|
}
|
|
39154
39321
|
});
|
|
39155
39322
|
}
|
|
@@ -43455,7 +43622,7 @@
|
|
|
43455
43622
|
super(renderTarget);
|
|
43456
43623
|
this.chartOptions = new ChartOptions(); // TODO handle onkeyup and oninput in chart options
|
|
43457
43624
|
}
|
|
43458
|
-
render(
|
|
43625
|
+
render(chartOptions) {
|
|
43459
43626
|
this.chartOptions.setOptions(chartOptions);
|
|
43460
43627
|
let targetElement = select(this.renderTarget);
|
|
43461
43628
|
targetElement.html("");
|
|
@@ -43809,20 +43976,102 @@
|
|
|
43809
43976
|
}
|
|
43810
43977
|
}
|
|
43811
43978
|
|
|
43979
|
+
// Centralized renderer for the hierarchy tree. Keeps a stable D3 data-join and
|
|
43980
|
+
// updates existing DOM nodes instead of fully recreating them on each render.
|
|
43981
|
+
class TreeRenderer {
|
|
43982
|
+
static render(owner, data, target) {
|
|
43983
|
+
// Ensure an <ul> exists for this target (one list per level)
|
|
43984
|
+
let list = target.select('ul');
|
|
43985
|
+
if (list.empty()) {
|
|
43986
|
+
list = target.append('ul').attr('role', target === owner.hierarchyElem ? 'tree' : 'group');
|
|
43987
|
+
}
|
|
43988
|
+
const entries = Object.keys(data).map(k => ({ key: k, item: data[k] }));
|
|
43989
|
+
const liSelection = list.selectAll('li').data(entries, (d) => d && d.key);
|
|
43990
|
+
liSelection.exit().remove();
|
|
43991
|
+
const liEnter = liSelection.enter().append('li')
|
|
43992
|
+
.attr('role', 'none')
|
|
43993
|
+
.classed('tsi-leaf', (d) => !!d.item.isLeaf);
|
|
43994
|
+
const liMerged = liEnter.merge(liSelection);
|
|
43995
|
+
const setSize = entries.length;
|
|
43996
|
+
liMerged.each((d, i, nodes) => {
|
|
43997
|
+
const entry = d;
|
|
43998
|
+
const li = select(nodes[i]);
|
|
43999
|
+
if (owner.selectedIds && owner.selectedIds.includes(entry.item.id)) {
|
|
44000
|
+
li.classed('tsi-selected', true);
|
|
44001
|
+
}
|
|
44002
|
+
else {
|
|
44003
|
+
li.classed('tsi-selected', false);
|
|
44004
|
+
}
|
|
44005
|
+
// determine instance vs hierarchy node by presence of isLeaf flag
|
|
44006
|
+
const isInstance = !!entry.item.isLeaf;
|
|
44007
|
+
const nodeNameToCheckIfExists = isInstance ? owner.instanceNodeString(entry.item) : entry.key;
|
|
44008
|
+
const displayName = (entry.item && (entry.item.displayName || nodeNameToCheckIfExists)) || nodeNameToCheckIfExists;
|
|
44009
|
+
li.attr('data-display-name', displayName);
|
|
44010
|
+
let itemElem = li.select('.tsi-hierarchyItem');
|
|
44011
|
+
if (itemElem.empty()) {
|
|
44012
|
+
const newListElem = owner.createHierarchyItemElem(entry.item, entry.key);
|
|
44013
|
+
li.node().appendChild(newListElem.node());
|
|
44014
|
+
itemElem = li.select('.tsi-hierarchyItem');
|
|
44015
|
+
}
|
|
44016
|
+
itemElem.attr('aria-label', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
|
|
44017
|
+
itemElem.attr('title', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
|
|
44018
|
+
itemElem.attr('aria-expanded', String(entry.item.isExpanded));
|
|
44019
|
+
// accessibility: set treeitem level and position in set
|
|
44020
|
+
const ariaLevel = String(((entry.item && typeof entry.item.level === 'number') ? entry.item.level : 0) + 1);
|
|
44021
|
+
itemElem.attr('aria-level', ariaLevel);
|
|
44022
|
+
itemElem.attr('aria-posinset', String(i + 1));
|
|
44023
|
+
itemElem.attr('aria-setsize', String(setSize));
|
|
44024
|
+
if (!isInstance) {
|
|
44025
|
+
itemElem.select('.tsi-caret-icon').attr('style', `left: ${(entry.item.level) * 18 + 20}px`);
|
|
44026
|
+
itemElem.select('.tsi-name').text(entry.key);
|
|
44027
|
+
itemElem.select('.tsi-instanceCount').text(entry.item.cumulativeInstanceCount);
|
|
44028
|
+
}
|
|
44029
|
+
else {
|
|
44030
|
+
const nameSpan = itemElem.select('.tsi-name');
|
|
44031
|
+
nameSpan.html('');
|
|
44032
|
+
Utils.appendFormattedElementsFromString(nameSpan, owner.instanceNodeStringToDisplay(entry.item));
|
|
44033
|
+
}
|
|
44034
|
+
entry.item.node = li;
|
|
44035
|
+
if (entry.item.children) {
|
|
44036
|
+
entry.item.isExpanded = true;
|
|
44037
|
+
li.classed('tsi-expanded', true);
|
|
44038
|
+
// recurse using TreeRenderer to keep rendering logic centralized
|
|
44039
|
+
TreeRenderer.render(owner, entry.item.children, li);
|
|
44040
|
+
}
|
|
44041
|
+
else {
|
|
44042
|
+
li.classed('tsi-expanded', false);
|
|
44043
|
+
li.selectAll('ul').remove();
|
|
44044
|
+
}
|
|
44045
|
+
});
|
|
44046
|
+
}
|
|
44047
|
+
}
|
|
44048
|
+
|
|
43812
44049
|
class HierarchyNavigation extends Component {
|
|
43813
44050
|
constructor(renderTarget) {
|
|
43814
44051
|
super(renderTarget);
|
|
43815
44052
|
this.path = [];
|
|
44053
|
+
// debounce + request cancellation fields
|
|
44054
|
+
this.debounceTimer = null;
|
|
44055
|
+
this.debounceDelay = 250; // ms
|
|
44056
|
+
this.requestCounter = 0; // increments for each outgoing request
|
|
44057
|
+
this.latestRequestId = 0; // id of the most recent request
|
|
43816
44058
|
//selectedIds
|
|
43817
44059
|
this.selectedIds = [];
|
|
43818
44060
|
this.searchEnabled = true;
|
|
43819
|
-
this.
|
|
44061
|
+
this.autocompleteEnabled = true; // Enable/disable autocomplete suggestions
|
|
44062
|
+
// Search mode state
|
|
44063
|
+
this.isSearchMode = false;
|
|
44064
|
+
// Paths that should be auto-expanded (Set of path strings like "Factory North/Building A")
|
|
44065
|
+
this.pathsToAutoExpand = new Set();
|
|
44066
|
+
this.renderSearchResult = async (r, payload, target) => {
|
|
43820
44067
|
const hierarchyData = r.hierarchyNodes?.hits?.length
|
|
43821
44068
|
? this.fillDataRecursively(r.hierarchyNodes, payload, payload)
|
|
43822
44069
|
: {};
|
|
43823
44070
|
const instancesData = r.instances?.hits?.length
|
|
43824
44071
|
? r.instances.hits.reduce((acc, i) => {
|
|
43825
|
-
|
|
44072
|
+
const inst = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
|
|
44073
|
+
inst.displayName = this.instanceNodeStringToDisplay(i) || '';
|
|
44074
|
+
acc[this.instanceNodeIdentifier(i)] = inst;
|
|
43826
44075
|
return acc;
|
|
43827
44076
|
}, {})
|
|
43828
44077
|
: {};
|
|
@@ -43833,7 +44082,17 @@
|
|
|
43833
44082
|
}
|
|
43834
44083
|
hitCountElem.text(r.hierarchyNodes.hitCount);
|
|
43835
44084
|
}
|
|
43836
|
-
|
|
44085
|
+
const merged = { ...hierarchyData, ...instancesData };
|
|
44086
|
+
this.renderTree(merged, target);
|
|
44087
|
+
// Auto-expand nodes that should be expanded and load their children
|
|
44088
|
+
for (const key in hierarchyData) {
|
|
44089
|
+
const node = hierarchyData[key];
|
|
44090
|
+
if (node.isExpanded && !node.children) {
|
|
44091
|
+
// This node should be expanded but doesn't have children loaded yet
|
|
44092
|
+
// We need to trigger expansion after the node is rendered
|
|
44093
|
+
await this.autoExpandNode(node);
|
|
44094
|
+
}
|
|
44095
|
+
}
|
|
43837
44096
|
};
|
|
43838
44097
|
this.hierarchyNodeIdentifier = (hName) => {
|
|
43839
44098
|
return hName ? hName : '(' + this.getString("Empty") + ')';
|
|
@@ -43855,12 +44114,20 @@
|
|
|
43855
44114
|
const targetElement = select(this.renderTarget).text('');
|
|
43856
44115
|
this.hierarchyNavWrapper = this.createHierarchyNavWrapper(targetElement);
|
|
43857
44116
|
this.selectedIds = preselectedIds;
|
|
44117
|
+
// Allow disabling autocomplete via options
|
|
44118
|
+
if (hierarchyNavOptions.autocompleteEnabled !== undefined) {
|
|
44119
|
+
this.autocompleteEnabled = hierarchyNavOptions.autocompleteEnabled;
|
|
44120
|
+
}
|
|
44121
|
+
// Pre-compute paths that need to be auto-expanded for preselected instances
|
|
44122
|
+
if (preselectedIds && preselectedIds.length > 0) {
|
|
44123
|
+
await this.computePathsToAutoExpand(preselectedIds);
|
|
44124
|
+
}
|
|
43858
44125
|
//render search wrapper
|
|
43859
|
-
|
|
44126
|
+
this.renderSearchBox();
|
|
43860
44127
|
super.themify(this.hierarchyNavWrapper, this.chartOptions.theme);
|
|
43861
44128
|
const results = this.createResultsWrapper(this.hierarchyNavWrapper);
|
|
43862
44129
|
this.hierarchyElem = this.createHierarchyElem(results);
|
|
43863
|
-
this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
|
|
44130
|
+
await this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
|
|
43864
44131
|
}
|
|
43865
44132
|
createHierarchyNavWrapper(targetElement) {
|
|
43866
44133
|
return targetElement.append('div').attr('class', 'tsi-hierarchy-nav-wrapper');
|
|
@@ -43868,8 +44135,129 @@
|
|
|
43868
44135
|
createResultsWrapper(hierarchyNavWrapper) {
|
|
43869
44136
|
return hierarchyNavWrapper.append('div').classed('tsi-hierarchy-or-list-wrapper', true);
|
|
43870
44137
|
}
|
|
44138
|
+
// create hierarchy container and attach keyboard handler
|
|
43871
44139
|
createHierarchyElem(results) {
|
|
43872
|
-
|
|
44140
|
+
const sel = results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
|
|
44141
|
+
// attach keydown listener for keyboard navigation (delegated)
|
|
44142
|
+
// use native event to preserve focus handling
|
|
44143
|
+
const node = sel.node();
|
|
44144
|
+
if (node) {
|
|
44145
|
+
node.addEventListener('keydown', (ev) => this.onKeyDown(ev));
|
|
44146
|
+
}
|
|
44147
|
+
return sel;
|
|
44148
|
+
}
|
|
44149
|
+
// Keyboard navigation handlers and helpers
|
|
44150
|
+
onKeyDown(ev) {
|
|
44151
|
+
const key = ev.key;
|
|
44152
|
+
const active = document.activeElement;
|
|
44153
|
+
const container = this.hierarchyElem?.node();
|
|
44154
|
+
if (!container)
|
|
44155
|
+
return;
|
|
44156
|
+
const isInside = active && container.contains(active);
|
|
44157
|
+
if (!isInside && (key === 'ArrowDown' || key === 'ArrowUp')) {
|
|
44158
|
+
// focus first visible item on navigation keys
|
|
44159
|
+
const visible = this.getVisibleItemElems();
|
|
44160
|
+
if (visible.length) {
|
|
44161
|
+
this.focusItem(visible[0]);
|
|
44162
|
+
ev.preventDefault();
|
|
44163
|
+
}
|
|
44164
|
+
return;
|
|
44165
|
+
}
|
|
44166
|
+
if (!active)
|
|
44167
|
+
return;
|
|
44168
|
+
const current = active.classList && active.classList.contains('tsi-hierarchyItem') ? active : active.closest('.tsi-hierarchyItem');
|
|
44169
|
+
if (!current)
|
|
44170
|
+
return;
|
|
44171
|
+
switch (key) {
|
|
44172
|
+
case 'ArrowDown':
|
|
44173
|
+
this.focusNext(current);
|
|
44174
|
+
ev.preventDefault();
|
|
44175
|
+
break;
|
|
44176
|
+
case 'ArrowUp':
|
|
44177
|
+
this.focusPrev(current);
|
|
44178
|
+
ev.preventDefault();
|
|
44179
|
+
break;
|
|
44180
|
+
case 'ArrowRight':
|
|
44181
|
+
this.handleArrowRight(current);
|
|
44182
|
+
ev.preventDefault();
|
|
44183
|
+
break;
|
|
44184
|
+
case 'ArrowLeft':
|
|
44185
|
+
this.handleArrowLeft(current);
|
|
44186
|
+
ev.preventDefault();
|
|
44187
|
+
break;
|
|
44188
|
+
case 'Enter':
|
|
44189
|
+
case ' ':
|
|
44190
|
+
// activate (toggle expand or select)
|
|
44191
|
+
current.click();
|
|
44192
|
+
ev.preventDefault();
|
|
44193
|
+
break;
|
|
44194
|
+
}
|
|
44195
|
+
}
|
|
44196
|
+
getVisibleItemElems() {
|
|
44197
|
+
if (!this.hierarchyElem)
|
|
44198
|
+
return [];
|
|
44199
|
+
const root = this.hierarchyElem.node();
|
|
44200
|
+
if (!root)
|
|
44201
|
+
return [];
|
|
44202
|
+
const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
|
|
44203
|
+
return items.filter(i => i.offsetParent !== null && getComputedStyle(i).display !== 'none');
|
|
44204
|
+
}
|
|
44205
|
+
focusItem(elem) {
|
|
44206
|
+
if (!this.hierarchyElem)
|
|
44207
|
+
return;
|
|
44208
|
+
const root = this.hierarchyElem.node();
|
|
44209
|
+
if (!root)
|
|
44210
|
+
return;
|
|
44211
|
+
const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
|
|
44212
|
+
items.forEach(i => i.setAttribute('tabindex', '-1'));
|
|
44213
|
+
elem.setAttribute('tabindex', '0');
|
|
44214
|
+
elem.focus();
|
|
44215
|
+
}
|
|
44216
|
+
focusNext(current) {
|
|
44217
|
+
const visible = this.getVisibleItemElems();
|
|
44218
|
+
const idx = visible.indexOf(current);
|
|
44219
|
+
if (idx >= 0 && idx < visible.length - 1) {
|
|
44220
|
+
this.focusItem(visible[idx + 1]);
|
|
44221
|
+
}
|
|
44222
|
+
}
|
|
44223
|
+
focusPrev(current) {
|
|
44224
|
+
const visible = this.getVisibleItemElems();
|
|
44225
|
+
const idx = visible.indexOf(current);
|
|
44226
|
+
if (idx > 0) {
|
|
44227
|
+
this.focusItem(visible[idx - 1]);
|
|
44228
|
+
}
|
|
44229
|
+
}
|
|
44230
|
+
handleArrowRight(current) {
|
|
44231
|
+
const caret = current.querySelector('.tsi-caret-icon');
|
|
44232
|
+
const expanded = current.getAttribute('aria-expanded') === 'true';
|
|
44233
|
+
if (caret && !expanded) {
|
|
44234
|
+
// expand
|
|
44235
|
+
current.click();
|
|
44236
|
+
return;
|
|
44237
|
+
}
|
|
44238
|
+
// if already expanded, move to first child
|
|
44239
|
+
if (caret && expanded) {
|
|
44240
|
+
const li = current.closest('li');
|
|
44241
|
+
const childLi = li?.querySelector('ul > li');
|
|
44242
|
+
const childItem = childLi?.querySelector('.tsi-hierarchyItem');
|
|
44243
|
+
if (childItem)
|
|
44244
|
+
this.focusItem(childItem);
|
|
44245
|
+
}
|
|
44246
|
+
}
|
|
44247
|
+
handleArrowLeft(current) {
|
|
44248
|
+
const caret = current.querySelector('.tsi-caret-icon');
|
|
44249
|
+
const expanded = current.getAttribute('aria-expanded') === 'true';
|
|
44250
|
+
if (caret && expanded) {
|
|
44251
|
+
// collapse
|
|
44252
|
+
current.click();
|
|
44253
|
+
return;
|
|
44254
|
+
}
|
|
44255
|
+
// move focus to parent
|
|
44256
|
+
const li = current.closest('li');
|
|
44257
|
+
const parentLi = li?.parentElement?.closest('li');
|
|
44258
|
+
const parentItem = parentLi?.querySelector('.tsi-hierarchyItem');
|
|
44259
|
+
if (parentItem)
|
|
44260
|
+
this.focusItem(parentItem);
|
|
43873
44261
|
}
|
|
43874
44262
|
// prepares the parameters for search request
|
|
43875
44263
|
requestPayload(hierarchy = null) {
|
|
@@ -43878,32 +44266,7 @@
|
|
|
43878
44266
|
}
|
|
43879
44267
|
// renders tree for both 'Navigate' and 'Filter' mode (with Hierarchy View option selected), locInTarget refers to the 'show more' element -either hierarchy or instance- within the target
|
|
43880
44268
|
renderTree(data, target) {
|
|
43881
|
-
|
|
43882
|
-
Object.keys(data).forEach(el => {
|
|
43883
|
-
let nodeNameToCheckIfExists = data[el] instanceof InstanceNode ? this.instanceNodeString(data[el]) : el;
|
|
43884
|
-
let li;
|
|
43885
|
-
if (list.selectAll(".tsi-name").nodes().find(e => e.innerText === nodeNameToCheckIfExists)) {
|
|
43886
|
-
li = null;
|
|
43887
|
-
}
|
|
43888
|
-
else {
|
|
43889
|
-
li = list.append('li').classed('tsi-leaf', data[el].isLeaf);
|
|
43890
|
-
//if the node is already selected, we want to highlight it
|
|
43891
|
-
if (this.selectedIds && this.selectedIds.includes(data[el].id)) {
|
|
43892
|
-
li.classed('tsi-selected', true);
|
|
43893
|
-
}
|
|
43894
|
-
}
|
|
43895
|
-
if (!li)
|
|
43896
|
-
return;
|
|
43897
|
-
li.attr("role", "none");
|
|
43898
|
-
let newListElem = this.createHierarchyItemElem(data[el], el);
|
|
43899
|
-
li.node().appendChild(newListElem.node());
|
|
43900
|
-
data[el].node = li;
|
|
43901
|
-
if (data[el].children) {
|
|
43902
|
-
data[el].isExpanded = true;
|
|
43903
|
-
data[el].node.classed('tsi-expanded', true);
|
|
43904
|
-
this.renderTree(data[el].children, data[el].node);
|
|
43905
|
-
}
|
|
43906
|
-
});
|
|
44269
|
+
TreeRenderer.render(this, data, target);
|
|
43907
44270
|
}
|
|
43908
44271
|
renderSearchBox() {
|
|
43909
44272
|
this.searchWrapperElem = this.hierarchyNavWrapper.append('div').classed('tsi-hierarchy-search', true);
|
|
@@ -43912,40 +44275,140 @@
|
|
|
43912
44275
|
let input = inputWrapper
|
|
43913
44276
|
.append("input")
|
|
43914
44277
|
.attr("class", "tsi-searchInput")
|
|
43915
|
-
.attr("aria-label", this.getString("Search
|
|
43916
|
-
.attr("aria-describedby", "tsi-search-desc")
|
|
44278
|
+
.attr("aria-label", this.getString("Search"))
|
|
44279
|
+
.attr("aria-describedby", "tsi-hierarchy-search-desc")
|
|
43917
44280
|
.attr("role", "combobox")
|
|
43918
44281
|
.attr("aria-owns", "tsi-search-results")
|
|
43919
44282
|
.attr("aria-expanded", "false")
|
|
43920
44283
|
.attr("aria-haspopup", "listbox")
|
|
43921
|
-
.attr("placeholder", this.getString("Search
|
|
44284
|
+
.attr("placeholder", this.getString("Search") + "...");
|
|
44285
|
+
// Add ARIA description for screen readers
|
|
44286
|
+
inputWrapper
|
|
44287
|
+
.append("span")
|
|
44288
|
+
.attr("id", "tsi-hierarchy-search-desc")
|
|
44289
|
+
.style("display", "none")
|
|
44290
|
+
.text(this.getString("Search suggestion instruction") || "Use arrow keys to navigate suggestions");
|
|
44291
|
+
// Add live region for search results info
|
|
44292
|
+
inputWrapper
|
|
44293
|
+
.append("div")
|
|
44294
|
+
.attr("class", "tsi-search-results-info")
|
|
44295
|
+
.attr("aria-live", "assertive");
|
|
44296
|
+
// Add clear button
|
|
44297
|
+
let clear = inputWrapper
|
|
44298
|
+
.append("div")
|
|
44299
|
+
.attr("class", "tsi-clear")
|
|
44300
|
+
.attr("tabindex", "0")
|
|
44301
|
+
.attr("role", "button")
|
|
44302
|
+
.attr("aria-label", "Clear Search")
|
|
44303
|
+
.on("click keydown", function (event) {
|
|
44304
|
+
if (Utils.isKeyDownAndNotEnter(event)) {
|
|
44305
|
+
return;
|
|
44306
|
+
}
|
|
44307
|
+
input.node().value = "";
|
|
44308
|
+
self.exitSearchMode();
|
|
44309
|
+
self.ap.close();
|
|
44310
|
+
select(this).classed("tsi-shown", false);
|
|
44311
|
+
});
|
|
44312
|
+
// Initialize Awesomplete for autocomplete (only if enabled)
|
|
44313
|
+
let Awesomplete = window.Awesomplete;
|
|
44314
|
+
if (this.autocompleteEnabled && Awesomplete) {
|
|
44315
|
+
this.ap = new Awesomplete(input.node(), {
|
|
44316
|
+
minChars: 1,
|
|
44317
|
+
maxItems: 10,
|
|
44318
|
+
autoFirst: true
|
|
44319
|
+
});
|
|
44320
|
+
}
|
|
44321
|
+
else {
|
|
44322
|
+
// Create a dummy object if autocomplete is disabled
|
|
44323
|
+
this.ap = {
|
|
44324
|
+
list: [],
|
|
44325
|
+
close: () => { },
|
|
44326
|
+
evaluate: () => { }
|
|
44327
|
+
};
|
|
44328
|
+
}
|
|
43922
44329
|
let self = this;
|
|
44330
|
+
let noSuggest = false;
|
|
44331
|
+
let justAwesompleted = false;
|
|
44332
|
+
// Handle autocomplete selection (only if enabled)
|
|
44333
|
+
if (this.autocompleteEnabled) {
|
|
44334
|
+
input.node().addEventListener("awesomplete-selectcomplete", (event) => {
|
|
44335
|
+
noSuggest = true;
|
|
44336
|
+
const selectedValue = event.text.value;
|
|
44337
|
+
// Trigger search with selected value
|
|
44338
|
+
self.performDeepSearch(selectedValue);
|
|
44339
|
+
justAwesompleted = true;
|
|
44340
|
+
});
|
|
44341
|
+
}
|
|
43923
44342
|
input.on("keydown", (event) => {
|
|
44343
|
+
// Handle ESC key to clear the search box
|
|
44344
|
+
if (event.key === 'Escape') {
|
|
44345
|
+
const inputElement = event.target;
|
|
44346
|
+
inputElement.value = '';
|
|
44347
|
+
// Trigger input event to clear search results
|
|
44348
|
+
self.exitSearchMode();
|
|
44349
|
+
self.ap.close();
|
|
44350
|
+
clear.classed("tsi-shown", false);
|
|
44351
|
+
return;
|
|
44352
|
+
}
|
|
43924
44353
|
this.chartOptions.onKeydown(event, this.ap);
|
|
43925
44354
|
});
|
|
43926
|
-
|
|
44355
|
+
input.node().addEventListener("keyup", function (event) {
|
|
44356
|
+
if (justAwesompleted) {
|
|
44357
|
+
justAwesompleted = false;
|
|
44358
|
+
return;
|
|
44359
|
+
}
|
|
44360
|
+
let key = event.which || event.keyCode;
|
|
44361
|
+
if (key === 13) {
|
|
44362
|
+
noSuggest = true;
|
|
44363
|
+
}
|
|
44364
|
+
});
|
|
44365
|
+
// Debounced input handler to reduce work while typing
|
|
43927
44366
|
input.on("input", function (event) {
|
|
43928
|
-
|
|
43929
|
-
|
|
43930
|
-
|
|
43931
|
-
self.
|
|
43932
|
-
self.
|
|
44367
|
+
const val = event.target.value;
|
|
44368
|
+
// always clear existing timer
|
|
44369
|
+
if (self.debounceTimer) {
|
|
44370
|
+
clearTimeout(self.debounceTimer);
|
|
44371
|
+
self.debounceTimer = null;
|
|
44372
|
+
}
|
|
44373
|
+
// Show/hide clear button
|
|
44374
|
+
clear.classed("tsi-shown", val.length > 0);
|
|
44375
|
+
if (!val || val.length === 0) {
|
|
44376
|
+
// Exit search mode and restore navigation view
|
|
44377
|
+
self.exitSearchMode();
|
|
44378
|
+
self.ap.close();
|
|
44379
|
+
return;
|
|
44380
|
+
}
|
|
44381
|
+
// Populate autocomplete suggestions with instance leaves (only if enabled)
|
|
44382
|
+
if (self.autocompleteEnabled && !noSuggest && val.length >= 1) {
|
|
44383
|
+
self.fetchAutocompleteSuggestions(val);
|
|
43933
44384
|
}
|
|
43934
44385
|
else {
|
|
43935
|
-
|
|
43936
|
-
self.filterTree(searchText);
|
|
44386
|
+
self.ap.close();
|
|
43937
44387
|
}
|
|
44388
|
+
// Use deep search for comprehensive results
|
|
44389
|
+
self.debounceTimer = setTimeout(() => {
|
|
44390
|
+
self.performDeepSearch(val);
|
|
44391
|
+
}, self.debounceDelay);
|
|
44392
|
+
noSuggest = false;
|
|
43938
44393
|
});
|
|
43939
44394
|
}
|
|
43940
44395
|
async pathSearchAndRenderResult({ search: { payload, bubbleUpReject = false }, render: { target, locInTarget = null } }) {
|
|
44396
|
+
const requestId = ++this.requestCounter;
|
|
44397
|
+
this.latestRequestId = requestId;
|
|
43941
44398
|
try {
|
|
43942
44399
|
const result = await this.searchFunction(payload);
|
|
44400
|
+
if (requestId !== this.latestRequestId) {
|
|
44401
|
+
return;
|
|
44402
|
+
}
|
|
43943
44403
|
if (result.error) {
|
|
43944
44404
|
throw result.error;
|
|
43945
44405
|
}
|
|
43946
|
-
this.renderSearchResult(result, payload, target);
|
|
44406
|
+
await this.renderSearchResult(result, payload, target);
|
|
43947
44407
|
}
|
|
43948
44408
|
catch (err) {
|
|
44409
|
+
if (requestId !== this.latestRequestId) {
|
|
44410
|
+
return;
|
|
44411
|
+
}
|
|
43949
44412
|
this.chartOptions.onError("Error in hierarchy navigation", "Failed to complete search", err instanceof XMLHttpRequest ? err : null);
|
|
43950
44413
|
if (bubbleUpReject) {
|
|
43951
44414
|
throw err;
|
|
@@ -43953,11 +44416,18 @@
|
|
|
43953
44416
|
}
|
|
43954
44417
|
}
|
|
43955
44418
|
filterTree(searchText) {
|
|
43956
|
-
|
|
43957
|
-
|
|
44419
|
+
const nodes = this.hierarchyElem.selectAll('ul').nodes();
|
|
44420
|
+
if (!nodes || !nodes.length)
|
|
44421
|
+
return;
|
|
44422
|
+
const tree = nodes[0];
|
|
44423
|
+
if (!tree)
|
|
44424
|
+
return;
|
|
44425
|
+
const list = tree.querySelectorAll('li');
|
|
44426
|
+
const needle = searchText.toLowerCase();
|
|
43958
44427
|
list.forEach((li) => {
|
|
43959
|
-
|
|
43960
|
-
|
|
44428
|
+
const attrName = li.getAttribute('data-display-name');
|
|
44429
|
+
let name = attrName && attrName.length ? attrName : (li.querySelector('.tsi-name')?.textContent || '');
|
|
44430
|
+
if (name.toLowerCase().includes(needle)) {
|
|
43961
44431
|
li.style.display = 'block';
|
|
43962
44432
|
}
|
|
43963
44433
|
else {
|
|
@@ -43965,11 +44435,300 @@
|
|
|
43965
44435
|
}
|
|
43966
44436
|
});
|
|
43967
44437
|
}
|
|
44438
|
+
// Fetch autocomplete suggestions for instances (leaves)
|
|
44439
|
+
async fetchAutocompleteSuggestions(searchText) {
|
|
44440
|
+
if (!searchText || searchText.length < 1) {
|
|
44441
|
+
this.ap.list = [];
|
|
44442
|
+
return;
|
|
44443
|
+
}
|
|
44444
|
+
try {
|
|
44445
|
+
// Call server search to get instance suggestions
|
|
44446
|
+
const payload = {
|
|
44447
|
+
path: this.path,
|
|
44448
|
+
searchTerm: searchText,
|
|
44449
|
+
recursive: true,
|
|
44450
|
+
includeInstances: true,
|
|
44451
|
+
// Limit results for autocomplete
|
|
44452
|
+
maxResults: 10
|
|
44453
|
+
};
|
|
44454
|
+
const results = await this.searchFunction(payload);
|
|
44455
|
+
if (results.error) {
|
|
44456
|
+
this.ap.list = [];
|
|
44457
|
+
return;
|
|
44458
|
+
}
|
|
44459
|
+
// Extract instance names for autocomplete suggestions
|
|
44460
|
+
const suggestions = [];
|
|
44461
|
+
if (results.instances?.hits) {
|
|
44462
|
+
results.instances.hits.forEach((i) => {
|
|
44463
|
+
const displayName = this.instanceNodeStringToDisplay(i);
|
|
44464
|
+
const pathStr = i.hierarchyPath && i.hierarchyPath.length > 0
|
|
44465
|
+
? i.hierarchyPath.join(' > ') + ' > '
|
|
44466
|
+
: '';
|
|
44467
|
+
suggestions.push({
|
|
44468
|
+
label: pathStr + displayName,
|
|
44469
|
+
value: displayName
|
|
44470
|
+
});
|
|
44471
|
+
});
|
|
44472
|
+
}
|
|
44473
|
+
// Update Awesomplete list
|
|
44474
|
+
this.ap.list = suggestions;
|
|
44475
|
+
}
|
|
44476
|
+
catch (err) {
|
|
44477
|
+
// Silently fail for autocomplete - don't interrupt user experience
|
|
44478
|
+
this.ap.list = [];
|
|
44479
|
+
}
|
|
44480
|
+
}
|
|
44481
|
+
// Perform deep search across entire hierarchy using server-side search
|
|
44482
|
+
async performDeepSearch(searchText) {
|
|
44483
|
+
if (!searchText || searchText.length < 2) {
|
|
44484
|
+
this.exitSearchMode();
|
|
44485
|
+
return;
|
|
44486
|
+
}
|
|
44487
|
+
this.isSearchMode = true;
|
|
44488
|
+
const requestId = ++this.requestCounter;
|
|
44489
|
+
this.latestRequestId = requestId;
|
|
44490
|
+
try {
|
|
44491
|
+
// Call server search with recursive flag
|
|
44492
|
+
const payload = {
|
|
44493
|
+
path: this.path,
|
|
44494
|
+
searchTerm: searchText,
|
|
44495
|
+
recursive: true, // Search entire subtree
|
|
44496
|
+
includeInstances: true
|
|
44497
|
+
};
|
|
44498
|
+
const results = await this.searchFunction(payload);
|
|
44499
|
+
if (requestId !== this.latestRequestId)
|
|
44500
|
+
return; // Stale request
|
|
44501
|
+
if (results.error) {
|
|
44502
|
+
throw results.error;
|
|
44503
|
+
}
|
|
44504
|
+
// Render search results in flat list view
|
|
44505
|
+
this.renderSearchResults(results, searchText);
|
|
44506
|
+
}
|
|
44507
|
+
catch (err) {
|
|
44508
|
+
if (requestId !== this.latestRequestId)
|
|
44509
|
+
return;
|
|
44510
|
+
this.chartOptions.onError("Search failed", "Unable to search hierarchy", err instanceof XMLHttpRequest ? err : null);
|
|
44511
|
+
}
|
|
44512
|
+
}
|
|
44513
|
+
// Render search results with breadcrumb paths
|
|
44514
|
+
renderSearchResults(results, searchText) {
|
|
44515
|
+
this.hierarchyElem.selectAll('*').remove();
|
|
44516
|
+
const flatResults = [];
|
|
44517
|
+
// Flatten hierarchy results with full paths
|
|
44518
|
+
if (results.hierarchyNodes?.hits) {
|
|
44519
|
+
results.hierarchyNodes.hits.forEach((h) => {
|
|
44520
|
+
flatResults.push({
|
|
44521
|
+
type: 'hierarchy',
|
|
44522
|
+
name: h.name,
|
|
44523
|
+
path: h.path || [],
|
|
44524
|
+
id: h.id,
|
|
44525
|
+
cumulativeInstanceCount: h.cumulativeInstanceCount,
|
|
44526
|
+
highlightedName: this.highlightMatch(h.name, searchText),
|
|
44527
|
+
node: h
|
|
44528
|
+
});
|
|
44529
|
+
});
|
|
44530
|
+
}
|
|
44531
|
+
// Flatten instance results with full paths
|
|
44532
|
+
if (results.instances?.hits) {
|
|
44533
|
+
results.instances.hits.forEach((i) => {
|
|
44534
|
+
const displayName = this.instanceNodeStringToDisplay(i);
|
|
44535
|
+
flatResults.push({
|
|
44536
|
+
type: 'instance',
|
|
44537
|
+
name: i.name,
|
|
44538
|
+
path: i.hierarchyPath || [],
|
|
44539
|
+
id: i.id,
|
|
44540
|
+
timeSeriesId: i.timeSeriesId,
|
|
44541
|
+
description: i.description,
|
|
44542
|
+
highlightedName: this.highlightMatch(displayName, searchText),
|
|
44543
|
+
node: i
|
|
44544
|
+
});
|
|
44545
|
+
});
|
|
44546
|
+
}
|
|
44547
|
+
// Render flat list with breadcrumbs
|
|
44548
|
+
const searchList = this.hierarchyElem
|
|
44549
|
+
.append('div')
|
|
44550
|
+
.classed('tsi-search-results', true);
|
|
44551
|
+
if (flatResults.length === 0) {
|
|
44552
|
+
searchList.append('div')
|
|
44553
|
+
.classed('tsi-noResults', true)
|
|
44554
|
+
.text(this.getString('No results'));
|
|
44555
|
+
return;
|
|
44556
|
+
}
|
|
44557
|
+
searchList.append('div')
|
|
44558
|
+
.classed('tsi-search-results-header', true)
|
|
44559
|
+
.html(`<strong>${flatResults.length}</strong> ${this.getString('results found') || 'results found'}`);
|
|
44560
|
+
const resultItems = searchList.selectAll('.tsi-search-result-item')
|
|
44561
|
+
.data(flatResults)
|
|
44562
|
+
.enter()
|
|
44563
|
+
.append('div')
|
|
44564
|
+
.classed('tsi-search-result-item', true)
|
|
44565
|
+
.attr('tabindex', '0')
|
|
44566
|
+
.attr('role', 'option')
|
|
44567
|
+
.attr('aria-label', (d) => {
|
|
44568
|
+
const pathStr = d.path.length > 0 ? d.path.join(' > ') + ' > ' : '';
|
|
44569
|
+
return pathStr + d.name;
|
|
44570
|
+
});
|
|
44571
|
+
const self = this;
|
|
44572
|
+
resultItems.each(function (d) {
|
|
44573
|
+
const item = select(this);
|
|
44574
|
+
// Breadcrumb path
|
|
44575
|
+
if (d.path.length > 0) {
|
|
44576
|
+
item.append('div')
|
|
44577
|
+
.classed('tsi-search-breadcrumb', true)
|
|
44578
|
+
.text(d.path.join(' > '));
|
|
44579
|
+
}
|
|
44580
|
+
// Highlighted name
|
|
44581
|
+
item.append('div')
|
|
44582
|
+
.classed('tsi-search-result-name', true)
|
|
44583
|
+
.html(d.highlightedName);
|
|
44584
|
+
// Instance description or count
|
|
44585
|
+
if (d.type === 'instance' && d.description) {
|
|
44586
|
+
item.append('div')
|
|
44587
|
+
.classed('tsi-search-result-description', true)
|
|
44588
|
+
.text(d.description);
|
|
44589
|
+
}
|
|
44590
|
+
else if (d.type === 'hierarchy') {
|
|
44591
|
+
item.append('div')
|
|
44592
|
+
.classed('tsi-search-result-count', true)
|
|
44593
|
+
.text(`${d.cumulativeInstanceCount || 0} instances`);
|
|
44594
|
+
}
|
|
44595
|
+
});
|
|
44596
|
+
// Click handlers
|
|
44597
|
+
resultItems.on('click keydown', function (event, d) {
|
|
44598
|
+
if (Utils.isKeyDownAndNotEnter(event))
|
|
44599
|
+
return;
|
|
44600
|
+
if (d.type === 'instance') {
|
|
44601
|
+
// Handle instance selection
|
|
44602
|
+
if (self.chartOptions.onInstanceClick) {
|
|
44603
|
+
const inst = new InstanceNode(d.timeSeriesId, d.name, d.path.length, d.id, d.description);
|
|
44604
|
+
// Update selection state
|
|
44605
|
+
if (self.selectedIds && self.selectedIds.includes(d.id)) {
|
|
44606
|
+
self.selectedIds = self.selectedIds.filter(id => id !== d.id);
|
|
44607
|
+
select(this).classed('tsi-selected', false);
|
|
44608
|
+
}
|
|
44609
|
+
else {
|
|
44610
|
+
self.selectedIds.push(d.id);
|
|
44611
|
+
select(this).classed('tsi-selected', true);
|
|
44612
|
+
}
|
|
44613
|
+
self.chartOptions.onInstanceClick(inst);
|
|
44614
|
+
}
|
|
44615
|
+
}
|
|
44616
|
+
else {
|
|
44617
|
+
// Navigate to hierarchy node - exit search and expand to that path
|
|
44618
|
+
self.navigateToPath(d.path);
|
|
44619
|
+
}
|
|
44620
|
+
});
|
|
44621
|
+
// Apply selection state to already-selected instances
|
|
44622
|
+
resultItems.each(function (d) {
|
|
44623
|
+
if (d.type === 'instance' && self.selectedIds && self.selectedIds.includes(d.id)) {
|
|
44624
|
+
select(this).classed('tsi-selected', true);
|
|
44625
|
+
}
|
|
44626
|
+
});
|
|
44627
|
+
}
|
|
44628
|
+
// Exit search mode and restore tree
|
|
44629
|
+
exitSearchMode() {
|
|
44630
|
+
this.isSearchMode = false;
|
|
44631
|
+
this.hierarchyElem.selectAll('*').remove();
|
|
44632
|
+
this.pathSearchAndRenderResult({
|
|
44633
|
+
search: { payload: this.requestPayload() },
|
|
44634
|
+
render: { target: this.hierarchyElem }
|
|
44635
|
+
});
|
|
44636
|
+
}
|
|
44637
|
+
// Navigate to a specific path in the hierarchy
|
|
44638
|
+
async navigateToPath(targetPath) {
|
|
44639
|
+
this.exitSearchMode();
|
|
44640
|
+
// For now, just exit search mode and return to root
|
|
44641
|
+
// In a more advanced implementation, this would progressively
|
|
44642
|
+
// expand nodes along the path to reveal the target
|
|
44643
|
+
// This would require waiting for each level to load before expanding the next
|
|
44644
|
+
}
|
|
44645
|
+
// Pre-compute which paths need to be auto-expanded for preselected instances
|
|
44646
|
+
async computePathsToAutoExpand(instanceIds) {
|
|
44647
|
+
if (!instanceIds || instanceIds.length === 0) {
|
|
44648
|
+
return;
|
|
44649
|
+
}
|
|
44650
|
+
// console.log('[HierarchyNavigation] Computing paths to auto-expand for:', instanceIds);
|
|
44651
|
+
try {
|
|
44652
|
+
this.pathsToAutoExpand.clear();
|
|
44653
|
+
for (const instanceId of instanceIds) {
|
|
44654
|
+
// Search for this specific instance
|
|
44655
|
+
const result = await this.searchFunction({
|
|
44656
|
+
path: this.path,
|
|
44657
|
+
searchTerm: instanceId,
|
|
44658
|
+
recursive: true,
|
|
44659
|
+
includeInstances: true
|
|
44660
|
+
});
|
|
44661
|
+
if (result?.instances?.hits) {
|
|
44662
|
+
for (const instance of result.instances.hits) {
|
|
44663
|
+
// Match by ID
|
|
44664
|
+
if (instance.id === instanceId ||
|
|
44665
|
+
(instance.id && instance.id.includes(instanceId))) {
|
|
44666
|
+
if (instance.hierarchyPath && instance.hierarchyPath.length > 0) {
|
|
44667
|
+
// Add all parent paths that need to be expanded
|
|
44668
|
+
const hierarchyPath = instance.hierarchyPath;
|
|
44669
|
+
for (let i = 1; i <= hierarchyPath.length; i++) {
|
|
44670
|
+
const pathArray = hierarchyPath.slice(0, i);
|
|
44671
|
+
const pathKey = pathArray.join('/');
|
|
44672
|
+
this.pathsToAutoExpand.add(pathKey);
|
|
44673
|
+
}
|
|
44674
|
+
}
|
|
44675
|
+
}
|
|
44676
|
+
}
|
|
44677
|
+
}
|
|
44678
|
+
}
|
|
44679
|
+
// console.log('[HierarchyNavigation] Paths to auto-expand:', Array.from(this.pathsToAutoExpand));
|
|
44680
|
+
}
|
|
44681
|
+
catch (err) {
|
|
44682
|
+
console.warn('Failed to compute paths to auto-expand:', err);
|
|
44683
|
+
}
|
|
44684
|
+
}
|
|
44685
|
+
// Check if a path should be auto-expanded
|
|
44686
|
+
shouldAutoExpand(pathArray) {
|
|
44687
|
+
if (this.pathsToAutoExpand.size === 0) {
|
|
44688
|
+
return false;
|
|
44689
|
+
}
|
|
44690
|
+
const pathKey = pathArray.join('/');
|
|
44691
|
+
return this.pathsToAutoExpand.has(pathKey);
|
|
44692
|
+
}
|
|
44693
|
+
// Auto-expand a node by triggering its expand function
|
|
44694
|
+
async autoExpandNode(node) {
|
|
44695
|
+
if (!node || !node.expand || !node.node) {
|
|
44696
|
+
return;
|
|
44697
|
+
}
|
|
44698
|
+
try {
|
|
44699
|
+
// Wait for the DOM node to be available
|
|
44700
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
44701
|
+
// Mark as expanded visually
|
|
44702
|
+
node.node.classed('tsi-expanded', true);
|
|
44703
|
+
// Call the expand function to load children
|
|
44704
|
+
await node.expand();
|
|
44705
|
+
// console.log(`[HierarchyNavigation] Auto-expanded node: ${node.path.join('/')}`);
|
|
44706
|
+
}
|
|
44707
|
+
catch (err) {
|
|
44708
|
+
console.warn(`Failed to auto-expand node ${node.path.join('/')}:`, err);
|
|
44709
|
+
}
|
|
44710
|
+
}
|
|
44711
|
+
// Highlight search term in text
|
|
44712
|
+
highlightMatch(text, searchTerm) {
|
|
44713
|
+
if (!text)
|
|
44714
|
+
return '';
|
|
44715
|
+
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44716
|
+
const regex = new RegExp(`(${escapedTerm})`, 'gi');
|
|
44717
|
+
return text.replace(regex, '<mark>$1</mark>');
|
|
44718
|
+
}
|
|
43968
44719
|
// creates in-depth data object using the server response for hierarchyNodes to show in the tree all expanded, considering UntilChildren
|
|
43969
44720
|
fillDataRecursively(hierarchyNodes, payload, payloadForContinuation = null) {
|
|
43970
44721
|
let data = {};
|
|
43971
44722
|
hierarchyNodes.hits.forEach((h) => {
|
|
43972
44723
|
let hierarchy = new HierarchyNode(h.name, payload.path, payload.path.length - this.path.length, h.cumulativeInstanceCount, h.id);
|
|
44724
|
+
// cache display name on node for client-side filtering
|
|
44725
|
+
hierarchy.displayName = h.name || '';
|
|
44726
|
+
// Check if this path should be auto-expanded
|
|
44727
|
+
const shouldExpand = this.shouldAutoExpand(hierarchy.path);
|
|
44728
|
+
if (shouldExpand) {
|
|
44729
|
+
hierarchy.isExpanded = true;
|
|
44730
|
+
//console.log(`[HierarchyNavigation] Auto-expanding node: ${hierarchy.path.join('/')}`);
|
|
44731
|
+
}
|
|
43973
44732
|
hierarchy.expand = () => {
|
|
43974
44733
|
hierarchy.isExpanded = true;
|
|
43975
44734
|
hierarchy.node.classed('tsi-expanded', true);
|
|
@@ -43993,7 +44752,7 @@
|
|
|
43993
44752
|
.attr('style', `padding-left: ${hORi.isLeaf ? hORi.level * 18 + 20 : (hORi.level + 1) * 18 + 20}px`)
|
|
43994
44753
|
.attr('tabindex', 0)
|
|
43995
44754
|
//.attr('arialabel', isHierarchyNode ? key : Utils.getTimeSeriesIdString(hORi))
|
|
43996
|
-
.attr('
|
|
44755
|
+
.attr('aria-label', isHierarchyNode ? key : self.getAriaLabel(hORi))
|
|
43997
44756
|
.attr('title', isHierarchyNode ? key : self.getAriaLabel(hORi))
|
|
43998
44757
|
.attr("role", "treeitem").attr('aria-expanded', hORi.isExpanded)
|
|
43999
44758
|
.on('click keydown', async function (event) {
|
|
@@ -44045,6 +44804,8 @@
|
|
|
44045
44804
|
return hORi.description || hORi.name || hORi.id || Utils.getTimeSeriesIdString(hORi);
|
|
44046
44805
|
}
|
|
44047
44806
|
}
|
|
44807
|
+
// TreeRenderer has been moved to its own module: ./TreeRenderer
|
|
44808
|
+
// The rendering logic was extracted to reduce file size and improve testability.
|
|
44048
44809
|
class HierarchyNode {
|
|
44049
44810
|
constructor(name, parentPath, level, cumulativeInstanceCount = null, id = null) {
|
|
44050
44811
|
this.name = name;
|
|
@@ -44289,6 +45050,7 @@
|
|
|
44289
45050
|
class DateTimeButtonSingle extends DateTimeButton {
|
|
44290
45051
|
constructor(renderTarget) {
|
|
44291
45052
|
super(renderTarget);
|
|
45053
|
+
this.clickOutsideHandler = null;
|
|
44292
45054
|
this.sDTPOnSet = (millis = null) => {
|
|
44293
45055
|
if (millis !== null) {
|
|
44294
45056
|
this.dateTimeButton.text(this.buttonDateTimeFormat(millis));
|
|
@@ -44301,6 +45063,32 @@
|
|
|
44301
45063
|
closeSDTP() {
|
|
44302
45064
|
this.dateTimePickerContainer.style("display", "none");
|
|
44303
45065
|
this.dateTimeButton.node().focus();
|
|
45066
|
+
this.removeClickOutsideHandler();
|
|
45067
|
+
}
|
|
45068
|
+
removeClickOutsideHandler() {
|
|
45069
|
+
if (this.clickOutsideHandler) {
|
|
45070
|
+
document.removeEventListener('click', this.clickOutsideHandler);
|
|
45071
|
+
this.clickOutsideHandler = null;
|
|
45072
|
+
}
|
|
45073
|
+
}
|
|
45074
|
+
setupClickOutsideHandler() {
|
|
45075
|
+
// Remove any existing handler first
|
|
45076
|
+
this.removeClickOutsideHandler();
|
|
45077
|
+
// Add handler after a small delay to prevent the opening click from immediately closing the picker
|
|
45078
|
+
setTimeout(() => {
|
|
45079
|
+
this.clickOutsideHandler = (event) => {
|
|
45080
|
+
const pickerElement = this.dateTimePickerContainer.node();
|
|
45081
|
+
const buttonElement = this.dateTimeButton.node();
|
|
45082
|
+
const target = event.target;
|
|
45083
|
+
// Check if click is outside both the picker and the button
|
|
45084
|
+
if (pickerElement && buttonElement &&
|
|
45085
|
+
!pickerElement.contains(target) &&
|
|
45086
|
+
!buttonElement.contains(target)) {
|
|
45087
|
+
this.closeSDTP();
|
|
45088
|
+
}
|
|
45089
|
+
};
|
|
45090
|
+
document.addEventListener('click', this.clickOutsideHandler);
|
|
45091
|
+
}, 0);
|
|
44304
45092
|
}
|
|
44305
45093
|
render(chartOptions = {}, minMillis, maxMillis, selectedMillis = null, onSet = null) {
|
|
44306
45094
|
super.render(chartOptions, minMillis, maxMillis, onSet);
|
|
@@ -44310,12 +45098,11 @@
|
|
|
44310
45098
|
if (!this.dateTimePicker) {
|
|
44311
45099
|
this.dateTimePicker = new SingleDateTimePicker(this.dateTimePickerContainer.node());
|
|
44312
45100
|
}
|
|
44313
|
-
let targetElement = select(this.renderTarget);
|
|
44314
|
-
(targetElement.select(".tsi-dateTimePickerContainer")).selectAll("*");
|
|
44315
45101
|
this.dateTimeButton.on("click", () => {
|
|
44316
45102
|
this.chartOptions.dTPIsModal = true;
|
|
44317
45103
|
this.dateTimePickerContainer.style("display", "block");
|
|
44318
45104
|
this.dateTimePicker.render(this.chartOptions, this.minMillis, this.maxMillis, this.selectedMillis, this.sDTPOnSet);
|
|
45105
|
+
this.setupClickOutsideHandler();
|
|
44319
45106
|
});
|
|
44320
45107
|
}
|
|
44321
45108
|
}
|
|
@@ -44613,8 +45400,16 @@
|
|
|
44613
45400
|
class PlaybackControls extends Component {
|
|
44614
45401
|
constructor(renderTarget, initialTimeStamp = null) {
|
|
44615
45402
|
super(renderTarget);
|
|
44616
|
-
this.
|
|
44617
|
-
this.
|
|
45403
|
+
this.playbackInterval = null;
|
|
45404
|
+
this.playButton = null;
|
|
45405
|
+
this.handleElement = null;
|
|
45406
|
+
this.controlsContainer = null;
|
|
45407
|
+
this.track = null;
|
|
45408
|
+
this.selectTimeStampCallback = null;
|
|
45409
|
+
this.wasPlayingWhenDragStarted = false;
|
|
45410
|
+
this.rafId = null;
|
|
45411
|
+
this.handleRadius = PlaybackControls.CONSTANTS.HANDLE_RADIUS;
|
|
45412
|
+
this.minimumPlaybackInterval = PlaybackControls.CONSTANTS.MINIMUM_PLAYBACK_INTERVAL_MS;
|
|
44618
45413
|
this.playbackInterval = null;
|
|
44619
45414
|
this.selectedTimeStamp = initialTimeStamp;
|
|
44620
45415
|
}
|
|
@@ -44622,6 +45417,21 @@
|
|
|
44622
45417
|
return this.selectedTimeStamp;
|
|
44623
45418
|
}
|
|
44624
45419
|
render(start, end, onSelectTimeStamp, options, playbackSettings) {
|
|
45420
|
+
// Validate inputs
|
|
45421
|
+
if (!(start instanceof Date) || !(end instanceof Date)) {
|
|
45422
|
+
throw new TypeError('start and end must be Date objects');
|
|
45423
|
+
}
|
|
45424
|
+
if (start >= end) {
|
|
45425
|
+
throw new RangeError('start must be before end');
|
|
45426
|
+
}
|
|
45427
|
+
if (!onSelectTimeStamp || typeof onSelectTimeStamp !== 'function') {
|
|
45428
|
+
throw new TypeError('onSelectTimeStamp must be a function');
|
|
45429
|
+
}
|
|
45430
|
+
// Clean up any pending animation frames before re-rendering
|
|
45431
|
+
if (this.rafId !== null) {
|
|
45432
|
+
cancelAnimationFrame(this.rafId);
|
|
45433
|
+
this.rafId = null;
|
|
45434
|
+
}
|
|
44625
45435
|
this.end = end;
|
|
44626
45436
|
this.selectTimeStampCallback = onSelectTimeStamp;
|
|
44627
45437
|
this.chartOptions.setOptions(options);
|
|
@@ -44683,6 +45493,9 @@
|
|
|
44683
45493
|
this.playButton = this.controlsContainer.append('button')
|
|
44684
45494
|
.classed('tsi-play-button', this.playbackInterval === null)
|
|
44685
45495
|
.classed('tsi-pause-button', this.playbackInterval !== null)
|
|
45496
|
+
// Accessibility attributes
|
|
45497
|
+
.attr('aria-label', 'Play/Pause playback')
|
|
45498
|
+
.attr('title', 'Play/Pause playback')
|
|
44686
45499
|
.on('click', () => {
|
|
44687
45500
|
if (this.playbackInterval === null) {
|
|
44688
45501
|
this.play();
|
|
@@ -44734,6 +45547,27 @@
|
|
|
44734
45547
|
this.updateSelection(handlePosition, this.selectedTimeStamp);
|
|
44735
45548
|
this.selectTimeStampCallback(this.selectedTimeStamp);
|
|
44736
45549
|
}
|
|
45550
|
+
/**
|
|
45551
|
+
* Cleanup resources to prevent memory leaks
|
|
45552
|
+
*/
|
|
45553
|
+
destroy() {
|
|
45554
|
+
this.pause();
|
|
45555
|
+
// Cancel any pending animation frames
|
|
45556
|
+
if (this.rafId !== null) {
|
|
45557
|
+
cancelAnimationFrame(this.rafId);
|
|
45558
|
+
this.rafId = null;
|
|
45559
|
+
}
|
|
45560
|
+
// Remove event listeners
|
|
45561
|
+
if (this.controlsContainer) {
|
|
45562
|
+
this.controlsContainer.selectAll('*').on('.', null);
|
|
45563
|
+
}
|
|
45564
|
+
// Clear DOM references
|
|
45565
|
+
this.playButton = null;
|
|
45566
|
+
this.handleElement = null;
|
|
45567
|
+
this.controlsContainer = null;
|
|
45568
|
+
this.track = null;
|
|
45569
|
+
this.selectTimeStampCallback = null;
|
|
45570
|
+
}
|
|
44737
45571
|
clamp(number, min, max) {
|
|
44738
45572
|
let clamped = Math.max(number, min);
|
|
44739
45573
|
return Math.min(clamped, max);
|
|
@@ -44742,9 +45576,17 @@
|
|
|
44742
45576
|
this.wasPlayingWhenDragStarted = this.wasPlayingWhenDragStarted ||
|
|
44743
45577
|
(this.playbackInterval !== null);
|
|
44744
45578
|
this.pause();
|
|
44745
|
-
|
|
44746
|
-
|
|
44747
|
-
this.
|
|
45579
|
+
// Use requestAnimationFrame to batch DOM updates for better performance
|
|
45580
|
+
// Cancel any pending animation frame to prevent stacking updates
|
|
45581
|
+
if (this.rafId !== null) {
|
|
45582
|
+
cancelAnimationFrame(this.rafId);
|
|
45583
|
+
}
|
|
45584
|
+
this.rafId = requestAnimationFrame(() => {
|
|
45585
|
+
const handlePosition = this.clamp(positionX, 0, this.trackWidth);
|
|
45586
|
+
this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
|
|
45587
|
+
this.updateSelection(handlePosition, this.selectedTimeStamp);
|
|
45588
|
+
this.rafId = null;
|
|
45589
|
+
});
|
|
44748
45590
|
}
|
|
44749
45591
|
onDragEnd() {
|
|
44750
45592
|
this.selectTimeStampCallback(this.selectedTimeStamp);
|
|
@@ -44767,6 +45609,12 @@
|
|
|
44767
45609
|
.text(this.timeFormatter(timeStamp));
|
|
44768
45610
|
}
|
|
44769
45611
|
}
|
|
45612
|
+
PlaybackControls.CONSTANTS = {
|
|
45613
|
+
HANDLE_RADIUS: 7,
|
|
45614
|
+
MINIMUM_PLAYBACK_INTERVAL_MS: 1000,
|
|
45615
|
+
HANDLE_PADDING: 8,
|
|
45616
|
+
AXIS_OFFSET: 6,
|
|
45617
|
+
};
|
|
44770
45618
|
class TimeAxis extends TemporalXAxisComponent {
|
|
44771
45619
|
constructor(renderTarget) {
|
|
44772
45620
|
super(renderTarget);
|