tsichart-core 2.0.0-beta.7 → 2.1.0

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.umd.js CHANGED
@@ -29013,7 +29013,7 @@
29013
29013
  swimLaneLabelHeightPadding: 8,
29014
29014
  labelLeftPadding: 28
29015
29015
  };
29016
- const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', ']', '}', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
29016
+ const CharactersToEscapeForExactSearchInstance = ['"', '`', '\'', '!', '(', ')', '^', '[', '{', ':', '}', ']', '~', '/', '\\', '@', '#', '$', '%', '&', '*', ';', '=', '.', '_', '-', '<', '>', ',', '?'];
29017
29017
  const NONNUMERICTOPMARGIN = 8;
29018
29018
  const LINECHARTTOPPADDING = 16;
29019
29019
  const GRIDCONTAINERCLASS = 'tsi-gridContainer';
@@ -29457,50 +29457,89 @@
29457
29457
  }
29458
29458
  return hclColor.toString();
29459
29459
  }
29460
+ /**
29461
+ * Creates an array of colors for split-by series
29462
+ * @param displayState - The current display state
29463
+ * @param aggKey - The aggregate key
29464
+ * @param ignoreIsOnlyAgg - Whether to ignore the "only aggregate" optimization
29465
+ * @returns Array of color strings for each split-by
29466
+ */
29460
29467
  static createSplitByColors(displayState, aggKey, ignoreIsOnlyAgg = false) {
29461
- if (Object.keys(displayState[aggKey]["splitBys"]).length == 1)
29468
+ const splitBys = displayState[aggKey]?.splitBys;
29469
+ if (!splitBys) {
29470
+ return [];
29471
+ }
29472
+ const splitByCount = Object.keys(splitBys).length;
29473
+ // Early return for single split-by
29474
+ if (splitByCount === 1) {
29462
29475
  return [displayState[aggKey].color];
29463
- var isOnlyAgg = Object.keys(displayState).reduce((accum, currAgg) => {
29464
- if (currAgg == aggKey)
29465
- return accum;
29466
- if (displayState[currAgg]["visible"] == false)
29467
- return accum && true;
29468
- return false;
29469
- }, true);
29470
- if (isOnlyAgg && !ignoreIsOnlyAgg) {
29471
- return this.generateColors(Object.keys(displayState[aggKey]["splitBys"]).length);
29472
29476
  }
29473
- var aggColor = displayState[aggKey].color;
29474
- var interpolateColor = linear().domain([0, Object.keys(displayState[aggKey]["splitBys"]).length])
29475
- .range([hcl$2(aggColor).darker().l, hcl$2(aggColor).brighter().l]);
29476
- var colors = [];
29477
- for (var i = 0; i < Object.keys(displayState[aggKey]["splitBys"]).length; i++) {
29478
- const newColor = hcl$2(aggColor);
29477
+ // Create cache key for memoization
29478
+ const cacheKey = `${aggKey}_${splitByCount}_${displayState[aggKey].color}_${ignoreIsOnlyAgg}`;
29479
+ if (this.splitByColorCache.has(cacheKey)) {
29480
+ return this.splitByColorCache.get(cacheKey);
29481
+ }
29482
+ const isOnlyVisibleAgg = !ignoreIsOnlyAgg && this.isOnlyVisibleAggregate(displayState, aggKey);
29483
+ let colors;
29484
+ if (isOnlyVisibleAgg) {
29485
+ // Generate distinct colors when this is the only visible aggregate
29486
+ colors = this.generateColors(splitByCount);
29487
+ }
29488
+ else {
29489
+ // Generate color variations based on aggregate color
29490
+ colors = this.generateSplitByColorVariations(displayState[aggKey].color, splitByCount);
29491
+ }
29492
+ // Cache the result
29493
+ this.splitByColorCache.set(cacheKey, colors);
29494
+ // Limit cache size to prevent memory leaks
29495
+ if (this.splitByColorCache.size > 100) {
29496
+ const firstKey = this.splitByColorCache.keys().next().value;
29497
+ this.splitByColorCache.delete(firstKey);
29498
+ }
29499
+ return colors;
29500
+ }
29501
+ /**
29502
+ * Helper method to check if an aggregate is the only visible one
29503
+ */
29504
+ static isOnlyVisibleAggregate(displayState, aggKey) {
29505
+ for (const currAgg in displayState) {
29506
+ if (currAgg !== aggKey && displayState[currAgg]?.visible !== false) {
29507
+ return false;
29508
+ }
29509
+ }
29510
+ return true;
29511
+ }
29512
+ /**
29513
+ * Helper method to generate color variations for split-bys
29514
+ */
29515
+ static generateSplitByColorVariations(baseColor, count) {
29516
+ const baseHcl = hcl$2(baseColor);
29517
+ const interpolateColor = linear()
29518
+ .domain([0, count])
29519
+ .range([baseHcl.darker().l, baseHcl.brighter().l]);
29520
+ const colors = new Array(count);
29521
+ for (let i = 0; i < count; i++) {
29522
+ const newColor = hcl$2(baseColor);
29479
29523
  newColor.l = interpolateColor(i);
29480
- colors.push(newColor.formatHex());
29524
+ colors[i] = newColor.formatHex();
29481
29525
  }
29482
29526
  return colors;
29483
29527
  }
29528
+ /**
29529
+ * Clears the split-by color cache (useful when display state changes significantly)
29530
+ */
29531
+ static clearSplitByColorCache() {
29532
+ this.splitByColorCache.clear();
29533
+ }
29484
29534
  static colorSplitBy(displayState, splitByIndex, aggKey, ignoreIsOnlyAgg = false) {
29485
- if (Object.keys(displayState[aggKey]["splitBys"]).length == 1)
29486
- return displayState[aggKey].color;
29487
- var isOnlyAgg = Object.keys(displayState).reduce((accum, currAgg) => {
29488
- if (currAgg == aggKey)
29489
- return accum;
29490
- if (displayState[currAgg]["visible"] == false)
29491
- return accum && true;
29492
- return false;
29493
- }, true);
29494
- if (isOnlyAgg && !ignoreIsOnlyAgg) {
29495
- var splitByColors = this.generateColors(Object.keys(displayState[aggKey]["splitBys"]).length);
29496
- return splitByColors[splitByIndex];
29535
+ const colors = this.createSplitByColors(displayState, aggKey, ignoreIsOnlyAgg);
29536
+ if (typeof splitByIndex === 'number' &&
29537
+ Number.isInteger(splitByIndex) &&
29538
+ splitByIndex >= 0 &&
29539
+ splitByIndex < colors.length) {
29540
+ return colors[splitByIndex];
29497
29541
  }
29498
- var aggColor = displayState[aggKey].color;
29499
- var interpolateColor = linear().domain([0, Object.keys(displayState[aggKey]["splitBys"]).length])
29500
- .range([hcl$2(aggColor).darker().l, hcl$2(aggColor).brighter().l]);
29501
- const newColor = hcl$2(aggColor);
29502
- newColor.l = interpolateColor(splitByIndex);
29503
- return newColor.formatHex();
29542
+ return displayState[aggKey]?.color || '#000000';
29504
29543
  }
29505
29544
  static getTheme(theme) {
29506
29545
  return theme ? 'tsi-' + theme : 'tsi-dark';
@@ -29924,6 +29963,7 @@
29924
29963
  }
29925
29964
  }
29926
29965
  Utils.guidForNullTSID = Utils.guid();
29966
+ Utils.splitByColorCache = new Map();
29927
29967
  Utils.equalToEventTarget = (function (current, event) {
29928
29968
  return (current == event.target);
29929
29969
  });
@@ -30409,35 +30449,41 @@
30409
30449
  }
30410
30450
  }
30411
30451
 
30412
- const NUMERICSPLITBYHEIGHT = 44;
30413
- const NONNUMERICSPLITBYHEIGHT = 24;
30452
+ /**
30453
+ * Constants for Legend component layout and behavior
30454
+ */
30455
+ const LEGEND_CONSTANTS = {
30456
+ /** Height in pixels for each numeric split-by item (includes type selector dropdown) */
30457
+ NUMERIC_SPLITBY_HEIGHT: 44,
30458
+ /** Height in pixels for each non-numeric (categorical/events) split-by item */
30459
+ NON_NUMERIC_SPLITBY_HEIGHT: 24,
30460
+ /** Height in pixels for the series name label header */
30461
+ NAME_LABEL_HEIGHT: 24,
30462
+ /** Buffer distance in pixels from scroll edge before triggering "load more" */
30463
+ SCROLL_BUFFER: 40,
30464
+ /** Number of split-by items to load per batch when paginating */
30465
+ BATCH_SIZE: 20,
30466
+ /** Minimum height in pixels for aggregate container */
30467
+ MIN_AGGREGATE_HEIGHT: 201,
30468
+ /** Minimum width in pixels for each series label in compact mode */
30469
+ MIN_SERIES_WIDTH: 124,
30470
+ };
30414
30471
  class Legend extends Component {
30415
30472
  constructor(drawChart, renderTarget, legendWidth) {
30416
30473
  super(renderTarget);
30417
30474
  this.renderSplitBys = (aggKey, aggSelection, dataType, noSplitBys) => {
30418
- var splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
30419
- var firstSplitBy = this.chartComponentData.displayState[aggKey].splitBys[Object.keys(this.chartComponentData.displayState[aggKey].splitBys)[0]];
30420
- var firstSplitByType = firstSplitBy ? firstSplitBy.visibleType : null;
30421
- Object.keys(this.chartComponentData.displayState[aggKey].splitBys).reduce((isSame, curr) => {
30422
- return (firstSplitByType == this.chartComponentData.displayState[aggKey].splitBys[curr].visibleType) && isSame;
30423
- }, true);
30424
- let showMoreSplitBys = () => {
30425
- const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
30426
- this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
30427
- if (oldShownSplitBys != this.chartComponentData.displayState[aggKey].shownSplitBys) {
30428
- this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30429
- }
30430
- };
30475
+ const splitByLabelData = Object.keys(this.chartComponentData.timeArrays[aggKey]);
30476
+ const showMoreSplitBys = () => this.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
30431
30477
  let splitByContainer = aggSelection.selectAll(".tsi-splitByContainer").data([aggKey]);
30432
- var splitByContainerEntered = splitByContainer.enter().append("div")
30478
+ const splitByContainerEntered = splitByContainer.enter().append("div")
30433
30479
  .merge(splitByContainer)
30434
30480
  .classed("tsi-splitByContainer", true);
30435
- var splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
30481
+ const splitByLabels = splitByContainerEntered.selectAll('.tsi-splitByLabel')
30436
30482
  .data(splitByLabelData.slice(0, this.chartComponentData.displayState[aggKey].shownSplitBys), function (d) {
30437
30483
  return d;
30438
30484
  });
30439
- let self = this;
30440
- var splitByLabelsEntered = splitByLabels
30485
+ const self = this;
30486
+ const splitByLabelsEntered = splitByLabels
30441
30487
  .enter()
30442
30488
  .append("div")
30443
30489
  .merge(splitByLabels)
@@ -30451,135 +30497,60 @@
30451
30497
  }
30452
30498
  })
30453
30499
  .on("click", function (event, splitBy) {
30454
- if (self.legendState == "compact") {
30455
- self.toggleSplitByVisible(aggKey, splitBy);
30456
- }
30457
- else {
30458
- self.toggleSticky(aggKey, splitBy);
30459
- }
30460
- self.drawChart();
30500
+ self.handleSplitByClick(aggKey, splitBy);
30461
30501
  })
30462
30502
  .on("mouseover", function (event, splitBy) {
30463
30503
  event.stopPropagation();
30464
- self.labelMouseover(aggKey, splitBy);
30504
+ self.handleSplitByMouseOver(aggKey, splitBy);
30465
30505
  })
30466
30506
  .on("mouseout", function (event) {
30467
30507
  event.stopPropagation();
30468
- self.svgSelection.selectAll(".tsi-valueElement")
30469
- .attr("stroke-opacity", 1)
30470
- .attr("fill-opacity", 1);
30471
- self.labelMouseout(self.svgSelection, aggKey);
30508
+ self.handleSplitByMouseOut(aggKey);
30472
30509
  })
30473
30510
  .attr("class", (splitBy, i) => {
30474
- let compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
30475
- let shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
30476
- return `tsi-splitByLabel tsi-splitByLabel ${compact} ${shown}`;
30511
+ const compact = (dataType !== DataTypes.Numeric) ? 'tsi-splitByLabelCompact' : '';
30512
+ const shown = Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy) ? 'shown' : '';
30513
+ return `tsi-splitByLabel ${compact} ${shown}`;
30477
30514
  })
30478
- .classed("stickied", (splitBy, i) => {
30479
- if (self.chartComponentData.stickiedKey != null) {
30480
- return aggKey == self.chartComponentData.stickiedKey.aggregateKey && splitBy == self.chartComponentData.stickiedKey.splitBy;
30481
- }
30482
- });
30483
- var colors = Utils.createSplitByColors(self.chartComponentData.displayState, aggKey, self.chartOptions.keepSplitByColor);
30515
+ .classed("stickied", (splitBy, i) => self.isStickied(aggKey, splitBy));
30516
+ // Use helper methods to render each split-by element
30484
30517
  splitByLabelsEntered.each(function (splitBy, j) {
30485
- let color = (self.chartComponentData.isFromHeatmap) ? self.chartComponentData.displayState[aggKey].color : colors[j];
30518
+ const selection = select(this);
30519
+ // Add color key (conditionally based on data type and legend state)
30486
30520
  if (dataType === DataTypes.Numeric || noSplitBys || self.legendState === 'compact') {
30487
- let colorKey = select(this).selectAll('.tsi-colorKey').data([color]);
30488
- let colorKeyEntered = colorKey.enter()
30489
- .append("div")
30490
- .attr("class", 'tsi-colorKey')
30491
- .merge(colorKey);
30492
- if (dataType === DataTypes.Numeric) {
30493
- colorKeyEntered.style('background-color', (d) => {
30494
- return d;
30495
- });
30496
- }
30497
- else {
30498
- self.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
30499
- }
30500
- select(this).classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && this.legendState !== 'compact');
30501
- colorKey.exit().remove();
30521
+ self.addColorKey(selection, aggKey, splitBy, dataType);
30522
+ selection.classed('tsi-nonCompactNonNumeric', (dataType === DataTypes.Categorical || dataType === DataTypes.Events) && self.legendState !== 'compact');
30502
30523
  }
30503
30524
  else {
30504
- select(this).selectAll('.tsi-colorKey').remove();
30505
- }
30506
- if (select(this).select('.tsi-eyeIcon').empty()) {
30507
- select(this).append("button")
30508
- .attr("class", "tsi-eyeIcon")
30509
- .attr('aria-label', () => {
30510
- let showOrHide = self.chartComponentData.displayState[aggKey].splitBys[splitBy].visible ? self.getString('hide series') : self.getString('show series');
30511
- return `${showOrHide} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`;
30512
- })
30513
- .attr('title', () => self.getString('Show/Hide values'))
30514
- .on("click", function (event) {
30515
- event.stopPropagation();
30516
- self.toggleSplitByVisible(aggKey, splitBy);
30517
- select(this)
30518
- .classed("shown", Utils.getAgVisible(self.chartComponentData.displayState, aggKey, splitBy));
30519
- self.drawChart();
30520
- });
30521
- }
30522
- if (select(this).select('.tsi-seriesName').empty()) {
30523
- let seriesName = select(this)
30524
- .append('div')
30525
- .attr('class', 'tsi-seriesName');
30526
- Utils.appendFormattedElementsFromString(seriesName, noSplitBys ? (self.chartComponentData.displayState[aggKey].name) : splitBy);
30525
+ selection.selectAll('.tsi-colorKey').remove();
30527
30526
  }
30527
+ // Add eye icon
30528
+ self.addEyeIcon(selection, aggKey, splitBy);
30529
+ // Add series name
30530
+ self.addSeriesName(selection, aggKey, splitBy);
30531
+ // Add series type selection for numeric data
30528
30532
  if (dataType === DataTypes.Numeric) {
30529
- if (select(this).select('.tsi-seriesTypeSelection').empty()) {
30530
- select(this).append("select")
30531
- .attr('aria-label', `${self.getString("Series type selection for")} ${splitBy} ${self.getString('in group')} ${self.chartComponentData.displayState[aggKey].name}`)
30532
- .attr('class', 'tsi-seriesTypeSelection')
30533
- .on("change", function (data) {
30534
- var seriesType = select(this).property("value");
30535
- self.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
30536
- self.drawChart();
30537
- })
30538
- .on("click", (event) => {
30539
- event.stopPropagation();
30540
- });
30541
- }
30542
- select(this).select('.tsi-seriesTypeSelection')
30543
- .each(function (d) {
30544
- var typeLabels = select(this).selectAll('option')
30545
- .data(data => self.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map((type) => {
30546
- return {
30547
- type: type,
30548
- aggKey: aggKey,
30549
- splitBy: splitBy,
30550
- visibleMeasure: Utils.getAgVisibleMeasure(self.chartComponentData.displayState, aggKey, splitBy)
30551
- };
30552
- }));
30553
- typeLabels
30554
- .enter()
30555
- .append("option")
30556
- .attr("class", "seriesTypeLabel")
30557
- .merge(typeLabels)
30558
- .property("selected", (data) => {
30559
- return ((data.type == Utils.getAgVisibleMeasure(self.chartComponentData.displayState, data.aggKey, data.splitBy)) ?
30560
- " selected" : "");
30561
- })
30562
- .text((data) => data.type);
30563
- typeLabels.exit().remove();
30564
- });
30533
+ self.addSeriesTypeSelection(selection, aggKey, splitBy);
30565
30534
  }
30566
30535
  else {
30567
- select(this).selectAll('.tsi-seriesTypeSelection').remove();
30536
+ selection.selectAll('.tsi-seriesTypeSelection').remove();
30568
30537
  }
30569
30538
  });
30570
30539
  splitByLabels.exit().remove();
30571
- let shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
30540
+ // Show more button
30541
+ const shouldShowMore = self.chartComponentData.displayState[aggKey].shownSplitBys < splitByLabelData.length;
30572
30542
  splitByContainerEntered.selectAll('.tsi-legendShowMore').remove();
30573
30543
  if (this.legendState === 'shown' && shouldShowMore) {
30574
30544
  splitByContainerEntered.append('button')
30575
30545
  .text(this.getString('Show more'))
30576
30546
  .attr('class', 'tsi-legendShowMore')
30577
- .style('display', (this.legendState === 'shown' && shouldShowMore) ? 'block' : 'none')
30547
+ .style('display', 'block')
30578
30548
  .on('click', showMoreSplitBys);
30579
30549
  }
30550
+ // Scroll handler for infinite scrolling
30580
30551
  splitByContainerEntered.on("scroll", function () {
30581
30552
  if (self.chartOptions.legend === 'shown') {
30582
- if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
30553
+ if (this.scrollTop + this.clientHeight + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollHeight) {
30583
30554
  showMoreSplitBys();
30584
30555
  }
30585
30556
  }
@@ -30604,10 +30575,125 @@
30604
30575
  };
30605
30576
  this.drawChart = drawChart;
30606
30577
  this.legendWidth = legendWidth;
30607
- this.legendElement = select(renderTarget).insert("div", ":first-child")
30578
+ this.legendElement = select(renderTarget)
30579
+ .insert("div", ":first-child")
30608
30580
  .attr("class", "tsi-legend")
30609
- .style("left", "0px")
30610
- .style("width", (this.legendWidth) + "px"); // - 16 for the width of the padding
30581
+ .style("left", "0px");
30582
+ // Note: width is set conditionally in draw() based on legendState
30583
+ // to allow CSS to control width in compact mode
30584
+ }
30585
+ getHeightPerSplitBy(aggKey) {
30586
+ const dataType = this.chartComponentData.displayState[aggKey].dataType;
30587
+ return dataType === DataTypes.Numeric
30588
+ ? LEGEND_CONSTANTS.NUMERIC_SPLITBY_HEIGHT
30589
+ : LEGEND_CONSTANTS.NON_NUMERIC_SPLITBY_HEIGHT;
30590
+ }
30591
+ addColorKey(selection, aggKey, splitBy, dataType) {
30592
+ const colors = Utils.createSplitByColors(this.chartComponentData.displayState, aggKey, this.chartOptions.keepSplitByColor);
30593
+ const splitByKeys = Object.keys(this.chartComponentData.timeArrays[aggKey]);
30594
+ const splitByIndex = splitByKeys.indexOf(splitBy);
30595
+ const color = this.chartComponentData.isFromHeatmap
30596
+ ? this.chartComponentData.displayState[aggKey].color
30597
+ : colors[splitByIndex];
30598
+ const colorKey = selection.selectAll('.tsi-colorKey').data([color]);
30599
+ const colorKeyEntered = colorKey.enter()
30600
+ .append('div')
30601
+ .attr('class', 'tsi-colorKey')
30602
+ .merge(colorKey);
30603
+ if (dataType === DataTypes.Numeric) {
30604
+ colorKeyEntered.style('background-color', d => d);
30605
+ }
30606
+ else {
30607
+ this.createNonNumericColorKey(dataType, colorKeyEntered, aggKey);
30608
+ }
30609
+ colorKey.exit().remove();
30610
+ }
30611
+ addEyeIcon(selection, aggKey, splitBy) {
30612
+ if (selection.select('.tsi-eyeIcon').empty()) {
30613
+ selection.append('button')
30614
+ .attr('class', 'tsi-eyeIcon')
30615
+ .attr('aria-label', () => {
30616
+ const showOrHide = this.chartComponentData.displayState[aggKey].splitBys[splitBy].visible
30617
+ ? this.getString('hide series')
30618
+ : this.getString('show series');
30619
+ return `${showOrHide} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`;
30620
+ })
30621
+ .attr('title', () => this.getString('Show/Hide values'))
30622
+ .on('click', (event) => {
30623
+ event.stopPropagation();
30624
+ this.toggleSplitByVisible(aggKey, splitBy);
30625
+ this.drawChart();
30626
+ });
30627
+ }
30628
+ selection.select('.tsi-eyeIcon')
30629
+ .classed('shown', Utils.getAgVisible(this.chartComponentData.displayState, aggKey, splitBy));
30630
+ }
30631
+ addSeriesName(selection, aggKey, splitBy) {
30632
+ if (selection.select('.tsi-seriesName').empty()) {
30633
+ const seriesName = selection.append('div')
30634
+ .attr('class', 'tsi-seriesName');
30635
+ const noSplitBys = Object.keys(this.chartComponentData.timeArrays[aggKey]).length === 1
30636
+ && Object.keys(this.chartComponentData.timeArrays[aggKey])[0] === '';
30637
+ const displayText = noSplitBys
30638
+ ? this.chartComponentData.displayState[aggKey].name
30639
+ : splitBy;
30640
+ Utils.appendFormattedElementsFromString(seriesName, displayText);
30641
+ }
30642
+ }
30643
+ addSeriesTypeSelection(selection, aggKey, splitBy) {
30644
+ if (selection.select('.tsi-seriesTypeSelection').empty()) {
30645
+ selection.append('select')
30646
+ .attr('aria-label', `${this.getString('Series type selection for')} ${splitBy} ${this.getString('in group')} ${this.chartComponentData.displayState[aggKey].name}`)
30647
+ .attr('class', 'tsi-seriesTypeSelection')
30648
+ .on('change', (event) => {
30649
+ const seriesType = select(event.target).property('value');
30650
+ this.chartComponentData.displayState[aggKey].splitBys[splitBy].visibleType = seriesType;
30651
+ this.drawChart();
30652
+ })
30653
+ .on('click', (event) => {
30654
+ event.stopPropagation();
30655
+ });
30656
+ }
30657
+ selection.select('.tsi-seriesTypeSelection')
30658
+ .each((d, i, nodes) => {
30659
+ const typeLabels = select(nodes[i])
30660
+ .selectAll('option')
30661
+ .data(this.chartComponentData.displayState[aggKey].splitBys[splitBy].types.map(type => ({
30662
+ type,
30663
+ aggKey,
30664
+ splitBy,
30665
+ visibleMeasure: Utils.getAgVisibleMeasure(this.chartComponentData.displayState, aggKey, splitBy)
30666
+ })));
30667
+ typeLabels.enter()
30668
+ .append('option')
30669
+ .attr('class', 'seriesTypeLabel')
30670
+ .merge(typeLabels)
30671
+ .property('selected', (data) => data.type === Utils.getAgVisibleMeasure(this.chartComponentData.displayState, data.aggKey, data.splitBy))
30672
+ .text((data) => data.type);
30673
+ typeLabels.exit().remove();
30674
+ });
30675
+ }
30676
+ handleSplitByClick(aggKey, splitBy) {
30677
+ if (this.legendState === 'compact') {
30678
+ this.toggleSplitByVisible(aggKey, splitBy);
30679
+ }
30680
+ else {
30681
+ this.toggleSticky(aggKey, splitBy);
30682
+ }
30683
+ this.drawChart();
30684
+ }
30685
+ handleSplitByMouseOver(aggKey, splitBy) {
30686
+ this.labelMouseover(aggKey, splitBy);
30687
+ }
30688
+ handleSplitByMouseOut(aggKey) {
30689
+ this.svgSelection.selectAll(".tsi-valueElement")
30690
+ .attr("stroke-opacity", 1)
30691
+ .attr("fill-opacity", 1);
30692
+ this.labelMouseout(this.svgSelection, aggKey);
30693
+ }
30694
+ isStickied(aggKey, splitBy) {
30695
+ const stickied = this.chartComponentData.stickiedKey;
30696
+ return stickied?.aggregateKey === aggKey && stickied?.splitBy === splitBy;
30611
30697
  }
30612
30698
  labelMouseoutWrapper(labelMouseout, svgSelection, event) {
30613
30699
  return (svgSelection, aggKey) => {
@@ -30649,14 +30735,11 @@
30649
30735
  return d == aggKey;
30650
30736
  }).node();
30651
30737
  var prospectiveScrollTop = Math.max((indexOfSplitBy - 1) * this.getHeightPerSplitBy(aggKey), 0);
30652
- if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - 40) || splitByNode.scrollTop > prospectiveScrollTop) {
30738
+ if (splitByNode.scrollTop < prospectiveScrollTop - (splitByNode.clientHeight - LEGEND_CONSTANTS.SCROLL_BUFFER) || splitByNode.scrollTop > prospectiveScrollTop) {
30653
30739
  splitByNode.scrollTop = prospectiveScrollTop;
30654
30740
  }
30655
30741
  }
30656
30742
  }
30657
- getHeightPerSplitBy(aggKey) {
30658
- return (this.chartComponentData.displayState[aggKey].dataType === DataTypes.Numeric ? NUMERICSPLITBYHEIGHT : NONNUMERICSPLITBYHEIGHT);
30659
- }
30660
30743
  createGradient(gradientKey, svg, values) {
30661
30744
  let gradient = svg.append('defs').append('linearGradient')
30662
30745
  .attr('id', gradientKey).attr('x1', '0%').attr('x2', '0%').attr('y1', '0%').attr('y2', '100%');
@@ -30675,10 +30758,6 @@
30675
30758
  .attr("stop-opacity", 1);
30676
30759
  });
30677
30760
  }
30678
- isNonNumeric(aggKey) {
30679
- let dataType = this.chartComponentData.displayState[aggKey].dataType;
30680
- return (dataType === DataTypes.Categorical || dataType === DataTypes.Events);
30681
- }
30682
30761
  createNonNumericColorKey(dataType, colorKey, aggKey) {
30683
30762
  if (dataType === DataTypes.Categorical) {
30684
30763
  this.createCategoricalColorKey(colorKey, aggKey);
@@ -30734,6 +30813,13 @@
30734
30813
  rect.attr('fill', "url(#" + gradientKey + ")");
30735
30814
  }
30736
30815
  }
30816
+ handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys) {
30817
+ const oldShownSplitBys = this.chartComponentData.displayState[aggKey].shownSplitBys;
30818
+ this.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + LEGEND_CONSTANTS.BATCH_SIZE, splitByLabelData.length);
30819
+ if (oldShownSplitBys !== this.chartComponentData.displayState[aggKey].shownSplitBys) {
30820
+ this.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30821
+ }
30822
+ }
30737
30823
  draw(legendState, chartComponentData, labelMouseover, svgSelection, options, labelMouseoutAction = null, stickySeriesAction = null, event) {
30738
30824
  this.chartOptions.setOptions(options);
30739
30825
  this.chartComponentData = chartComponentData;
@@ -30748,6 +30834,13 @@
30748
30834
  legend.style('visibility', this.legendState != 'hidden')
30749
30835
  .classed('compact', this.legendState == 'compact')
30750
30836
  .classed('hidden', this.legendState == 'hidden');
30837
+ // Set width conditionally - let CSS handle compact mode width
30838
+ if (this.legendState !== 'compact') {
30839
+ legend.style('width', `${this.legendWidth}px`);
30840
+ }
30841
+ else {
30842
+ legend.style('width', null); // Remove inline width style in compact mode
30843
+ }
30751
30844
  let seriesNames = Object.keys(this.chartComponentData.displayState);
30752
30845
  var seriesLabels = legend.selectAll(".tsi-seriesLabel")
30753
30846
  .data(seriesNames, d => d);
@@ -30758,7 +30851,7 @@
30758
30851
  return "tsi-seriesLabel " + (this.chartComponentData.displayState[d]["visible"] ? " shown" : "");
30759
30852
  })
30760
30853
  .style("min-width", () => {
30761
- return Math.min(124, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
30854
+ return Math.min(LEGEND_CONSTANTS.MIN_SERIES_WIDTH, this.legendElement.node().clientWidth / seriesNames.length) + 'px';
30762
30855
  })
30763
30856
  .style("border-color", function (d, i) {
30764
30857
  if (select(this).classed("shown"))
@@ -30766,9 +30859,8 @@
30766
30859
  return "lightgray";
30767
30860
  });
30768
30861
  var self = this;
30769
- const heightPerNameLabel = 25;
30770
30862
  const usableLegendHeight = legend.node().clientHeight;
30771
- var prospectiveAggregateHeight = Math.ceil(Math.max(201, (usableLegendHeight / seriesLabelsEntered.size())));
30863
+ var prospectiveAggregateHeight = Math.ceil(Math.max(LEGEND_CONSTANTS.MIN_AGGREGATE_HEIGHT, (usableLegendHeight / seriesLabelsEntered.size())));
30772
30864
  var contentHeight = 0;
30773
30865
  seriesLabelsEntered.each(function (aggKey, i) {
30774
30866
  let heightPerSplitBy = self.getHeightPerSplitBy(aggKey);
@@ -30824,12 +30916,12 @@
30824
30916
  seriesNameLabel.exit().remove();
30825
30917
  var splitByContainerHeight;
30826
30918
  if (splitByLabelData.length > (prospectiveAggregateHeight / heightPerSplitBy)) {
30827
- splitByContainerHeight = prospectiveAggregateHeight - heightPerNameLabel;
30828
- contentHeight += splitByContainerHeight + heightPerNameLabel;
30919
+ splitByContainerHeight = prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30920
+ contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30829
30921
  }
30830
30922
  else if (splitByLabelData.length > 1 || (splitByLabelData.length === 1 && splitByLabelData[0] !== "")) {
30831
- splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + heightPerNameLabel;
30832
- contentHeight += splitByContainerHeight + heightPerNameLabel;
30923
+ splitByContainerHeight = splitByLabelData.length * heightPerSplitBy + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30924
+ contentHeight += splitByContainerHeight + LEGEND_CONSTANTS.NAME_LABEL_HEIGHT;
30833
30925
  }
30834
30926
  else {
30835
30927
  splitByContainerHeight = heightPerSplitBy;
@@ -30842,43 +30934,28 @@
30842
30934
  select(this).style("height", "unset");
30843
30935
  }
30844
30936
  var splitByContainer = select(this).selectAll(".tsi-splitByContainer").data([aggKey]);
30845
- var splitByContainerEntered = splitByContainer.enter().append("div")
30937
+ splitByContainer.enter().append("div")
30846
30938
  .merge(splitByContainer)
30847
30939
  .classed("tsi-splitByContainer", true);
30848
30940
  let aggSelection = select(this);
30849
30941
  self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30850
- splitByContainerEntered.on("scroll", function () {
30851
- if (self.chartOptions.legend == "shown") {
30852
- if (this.scrollTop + this.clientHeight + 40 > this.scrollHeight) {
30853
- const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
30854
- self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
30855
- if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
30856
- self.renderSplitBys(aggKey, aggSelection, dataType, noSplitBys);
30857
- }
30858
- }
30859
- }
30860
- });
30942
+ // Compact mode horizontal scroll handler
30861
30943
  select(this).on('scroll', function () {
30862
30944
  if (self.chartOptions.legend == "compact") {
30863
- if (this.scrollLeft + this.clientWidth + 40 > this.scrollWidth) {
30864
- const oldShownSplitBys = self.chartComponentData.displayState[aggKey].shownSplitBys;
30865
- self.chartComponentData.displayState[aggKey].shownSplitBys = Math.min(oldShownSplitBys + 20, splitByLabelData.length);
30866
- if (oldShownSplitBys != self.chartComponentData.displayState[aggKey].shownSplitBys) {
30867
- this.renderSplitBys(dataType);
30868
- }
30945
+ if (this.scrollLeft + this.clientWidth + LEGEND_CONSTANTS.SCROLL_BUFFER > this.scrollWidth) {
30946
+ self.handleShowMoreSplitBys(aggKey, splitByLabelData, aggSelection, dataType, noSplitBys);
30869
30947
  }
30870
30948
  }
30871
30949
  });
30872
30950
  splitByContainer.exit().remove();
30873
30951
  });
30874
30952
  if (this.chartOptions.legend == 'shown') {
30875
- legend.node().clientHeight;
30876
30953
  //minSplitBysForFlexGrow: the minimum number of split bys for flex-grow to be triggered
30877
30954
  if (contentHeight < usableLegendHeight) {
30878
30955
  this.legendElement.classed("tsi-flexLegend", true);
30879
30956
  seriesLabelsEntered.each(function (d) {
30880
30957
  let heightPerSplitBy = self.getHeightPerSplitBy(d);
30881
- var minSplitByForFlexGrow = (prospectiveAggregateHeight - heightPerNameLabel) / heightPerSplitBy;
30958
+ var minSplitByForFlexGrow = (prospectiveAggregateHeight - LEGEND_CONSTANTS.NAME_LABEL_HEIGHT) / heightPerSplitBy;
30882
30959
  var splitBysCount = Object.keys(self.chartComponentData.displayState[String(select(this).data()[0])].splitBys).length;
30883
30960
  if (splitBysCount > minSplitByForFlexGrow) {
30884
30961
  select(this).style("flex-grow", 1);
@@ -30891,6 +30968,12 @@
30891
30968
  }
30892
30969
  seriesLabels.exit().remove();
30893
30970
  }
30971
+ destroy() {
30972
+ this.legendElement.remove();
30973
+ // Note: Virtual list cleanup will be added when virtual scrolling is implemented
30974
+ // this.virtualLists.forEach(list => list.destroy());
30975
+ // this.virtualLists.clear();
30976
+ }
30894
30977
  }
30895
30978
 
30896
30979
  class ChartComponentData {
@@ -33114,6 +33197,8 @@
33114
33197
  this.drawChart = drawChart;
33115
33198
  this.contextMenuElement = select(renderTarget).insert("div", ":first-child")
33116
33199
  .attr("class", "tsi-contextMenu")
33200
+ .attr("aria-label", "Context Menu")
33201
+ .attr("role", "menu")
33117
33202
  .style("left", "0px")
33118
33203
  .style("top", "0px");
33119
33204
  }
@@ -33202,6 +33287,7 @@
33202
33287
  var actionElementsEntered = actionElements.enter()
33203
33288
  .append("div")
33204
33289
  .attr("class", `tsi-actionElement`)
33290
+ .attr("role", "menuitem")
33205
33291
  .classed('tsi-hasSubMenu', d => d.isNested)
33206
33292
  .merge(actionElements)
33207
33293
  .text(d => d.name)
@@ -33328,6 +33414,7 @@
33328
33414
  }).data([theme]);
33329
33415
  this.tooltipDiv = tooltip.enter().append('div')
33330
33416
  .attr('class', 'tsi-tooltip')
33417
+ .attr('role', 'tooltip')
33331
33418
  .merge(tooltip)
33332
33419
  .each(function (d) {
33333
33420
  select(this).selectAll("*").remove();
@@ -36702,6 +36789,10 @@
36702
36789
  label.enter()
36703
36790
  .append("text")
36704
36791
  .attr("class", (d) => `tsi-swimLaneLabel-${lane} tsi-swimLaneLabel ${onClickPresentAndValid(d) ? 'tsi-boldOnHover' : ''}`)
36792
+ .attr("role", "heading")
36793
+ .attr("aria-roledescription", this.getString("Swimlane label"))
36794
+ .attr("aria-label", d => d.label)
36795
+ .attr("aria-level", "3")
36705
36796
  .merge(label)
36706
36797
  .style("text-anchor", "middle")
36707
36798
  .attr("transform", d => `translate(${(-this.horizontalLabelOffset + swimlaneLabelConstants.labelLeftPadding)},${(d.offset + d.height / 2)}) rotate(-90)`)
@@ -36726,13 +36817,12 @@
36726
36817
  });
36727
36818
  }
36728
36819
  render(data, options, aggregateExpressionOptions) {
36729
- console.log('LineChart render called a');
36730
36820
  super.render(data, options, aggregateExpressionOptions);
36731
36821
  this.originalSwimLanes = this.aggregateExpressionOptions.map((aEO) => {
36732
36822
  return aEO.swimLane;
36733
36823
  });
36734
36824
  this.originalSwimLaneOptions = options.swimLaneOptions;
36735
- this.hasBrush = options && (options.brushMoveAction || options.brushMoveEndAction || options.brushContextMenuActions);
36825
+ this.hasBrush = !!(options && (options.brushMoveAction || options.brushMoveEndAction || options.brushContextMenuActions));
36736
36826
  this.chartOptions.setOptions(options);
36737
36827
  this.chartMargins.right = this.chartOptions.labelSeriesWithMarker ? (SERIESLABELWIDTH + 8) : LINECHARTCHARTMARGINS.right;
36738
36828
  this.width = this.getWidth();
@@ -36781,6 +36871,7 @@
36781
36871
  .attr("type", "button")
36782
36872
  .on("click", function () {
36783
36873
  self.overwriteSwimLanes();
36874
+ // cast to any to avoid TS incompatibility when spreading chartOptions instance into ILineChartOptions
36784
36875
  self.render(self.data, { ...self.chartOptions, yAxisState: self.nextStackedState() }, self.aggregateExpressionOptions);
36785
36876
  select(this).attr("aria-label", () => self.getString("set axis state to") + ' ' + self.nextStackedState());
36786
36877
  setTimeout(() => select(this).node().focus(), 200);
@@ -36807,6 +36898,7 @@
36807
36898
  this.svgSelection = this.targetElement.append("svg")
36808
36899
  .attr("class", "tsi-lineChartSVG tsi-chartSVG")
36809
36900
  .attr('title', this.getString('Line chart'))
36901
+ .attr("role", "img")
36810
36902
  .attr("height", this.height);
36811
36903
  var g = this.svgSelection.append("g")
36812
36904
  .classed("svgGroup", true)
@@ -37192,1301 +37284,76 @@
37192
37284
  }
37193
37285
  }
37194
37286
 
37195
- var pikaday$1 = {exports: {}};
37196
-
37197
- /*!
37198
- * Pikaday
37199
- *
37200
- * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday
37201
- */
37202
- var pikaday = pikaday$1.exports;
37203
-
37204
- var hasRequiredPikaday;
37205
-
37206
- function requirePikaday () {
37207
- if (hasRequiredPikaday) return pikaday$1.exports;
37208
- hasRequiredPikaday = 1;
37209
- (function (module, exports) {
37210
- (function (root, factory)
37211
- {
37212
-
37213
- var moment;
37214
- {
37215
- // CommonJS module
37216
- // Load moment.js as an optional dependency
37217
- try { moment = requireMoment(); } catch (e) {}
37218
- module.exports = factory(moment);
37219
- }
37220
- }(pikaday, function (moment)
37221
- {
37222
-
37223
- /**
37224
- * feature detection and helper functions
37225
- */
37226
- var hasMoment = typeof moment === 'function',
37227
-
37228
- hasEventListeners = !!window.addEventListener,
37229
-
37230
- document = window.document,
37231
-
37232
- sto = window.setTimeout,
37233
-
37234
- addEvent = function(el, e, callback, capture)
37235
- {
37236
- if (hasEventListeners) {
37237
- el.addEventListener(e, callback, !!capture);
37238
- } else {
37239
- el.attachEvent('on' + e, callback);
37240
- }
37241
- },
37242
-
37243
- removeEvent = function(el, e, callback, capture)
37244
- {
37245
- if (hasEventListeners) {
37246
- el.removeEventListener(e, callback, !!capture);
37247
- } else {
37248
- el.detachEvent('on' + e, callback);
37249
- }
37250
- },
37251
-
37252
- trim = function(str)
37253
- {
37254
- return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,'');
37255
- },
37256
-
37257
- hasClass = function(el, cn)
37258
- {
37259
- return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1;
37260
- },
37261
-
37262
- addClass = function(el, cn)
37263
- {
37264
- if (!hasClass(el, cn)) {
37265
- el.className = (el.className === '') ? cn : el.className + ' ' + cn;
37266
- }
37267
- },
37268
-
37269
- removeClass = function(el, cn)
37270
- {
37271
- el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' '));
37272
- },
37273
-
37274
- isArray = function(obj)
37275
- {
37276
- return (/Array/).test(Object.prototype.toString.call(obj));
37277
- },
37278
-
37279
- isDate = function(obj)
37280
- {
37281
- return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime());
37282
- },
37283
-
37284
- isWeekend = function(date)
37285
- {
37286
- var day = date.getDay();
37287
- return day === 0 || day === 6;
37288
- },
37289
-
37290
- isLeapYear = function(year)
37291
- {
37292
- // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
37293
- return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
37294
- },
37295
-
37296
- getDaysInMonth = function(year, month)
37297
- {
37298
- return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
37299
- },
37300
-
37301
- setToStartOfDay = function(date)
37302
- {
37303
- if (isDate(date)) date.setHours(0,0,0,0);
37304
- },
37305
-
37306
- compareDates = function(a,b)
37307
- {
37308
- // weak date comparison (use setToStartOfDay(date) to ensure correct result)
37309
- return a.getTime() === b.getTime();
37310
- },
37311
-
37312
- extend = function(to, from, overwrite)
37313
- {
37314
- var prop, hasProp;
37315
- for (prop in from) {
37316
- hasProp = to[prop] !== undefined;
37317
- if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) {
37318
- if (isDate(from[prop])) {
37319
- if (overwrite) {
37320
- to[prop] = new Date(from[prop].getTime());
37321
- }
37322
- }
37323
- else if (isArray(from[prop])) {
37324
- if (overwrite) {
37325
- to[prop] = from[prop].slice(0);
37326
- }
37327
- } else {
37328
- to[prop] = extend({}, from[prop], overwrite);
37329
- }
37330
- } else if (overwrite || !hasProp) {
37331
- to[prop] = from[prop];
37332
- }
37333
- }
37334
- return to;
37335
- },
37336
-
37337
- fireEvent = function(el, eventName, data)
37338
- {
37339
- var ev;
37340
-
37341
- if (document.createEvent) {
37342
- ev = document.createEvent('HTMLEvents');
37343
- ev.initEvent(eventName, true, false);
37344
- ev = extend(ev, data);
37345
- el.dispatchEvent(ev);
37346
- } else if (document.createEventObject) {
37347
- ev = document.createEventObject();
37348
- ev = extend(ev, data);
37349
- el.fireEvent('on' + eventName, ev);
37350
- }
37351
- },
37352
-
37353
- adjustCalendar = function(calendar) {
37354
- if (calendar.month < 0) {
37355
- calendar.year -= Math.ceil(Math.abs(calendar.month)/12);
37356
- calendar.month += 12;
37357
- }
37358
- if (calendar.month > 11) {
37359
- calendar.year += Math.floor(Math.abs(calendar.month)/12);
37360
- calendar.month -= 12;
37361
- }
37362
- return calendar;
37363
- },
37364
-
37365
- /**
37366
- * defaults and localisation
37367
- */
37368
- defaults = {
37369
-
37370
- // bind the picker to a form field
37371
- field: null,
37372
-
37373
- // automatically show/hide the picker on `field` focus (default `true` if `field` is set)
37374
- bound: undefined,
37375
-
37376
- // position of the datepicker, relative to the field (default to bottom & left)
37377
- // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position)
37378
- position: 'bottom left',
37379
-
37380
- // automatically fit in the viewport even if it means repositioning from the position option
37381
- reposition: true,
37382
-
37383
- // the default output format for `.toString()` and `field` value
37384
- format: 'YYYY-MM-DD',
37385
-
37386
- // the toString function which gets passed a current date object and format
37387
- // and returns a string
37388
- toString: null,
37389
-
37390
- // used to create date object from current input string
37391
- parse: null,
37392
-
37393
- // the initial date to view when first opened
37394
- defaultDate: null,
37395
-
37396
- // make the `defaultDate` the initial selected value
37397
- setDefaultDate: false,
37398
-
37399
- // first day of week (0: Sunday, 1: Monday etc)
37400
- firstDay: 0,
37401
-
37402
- // the default flag for moment's strict date parsing
37403
- formatStrict: false,
37404
-
37405
- // the minimum/earliest date that can be selected
37406
- minDate: null,
37407
- // the maximum/latest date that can be selected
37408
- maxDate: null,
37409
-
37410
- // number of years either side, or array of upper/lower range
37411
- yearRange: 10,
37412
-
37413
- // show week numbers at head of row
37414
- showWeekNumber: false,
37415
-
37416
- // Week picker mode
37417
- pickWholeWeek: false,
37418
-
37419
- // used internally (don't config outside)
37420
- minYear: 0,
37421
- maxYear: 9999,
37422
- minMonth: undefined,
37423
- maxMonth: undefined,
37424
-
37425
- startRange: null,
37426
- endRange: null,
37427
-
37428
- isRTL: false,
37429
-
37430
- // Additional text to append to the year in the calendar title
37431
- yearSuffix: '',
37432
-
37433
- // Render the month after year in the calendar title
37434
- showMonthAfterYear: false,
37435
-
37436
- // Render days of the calendar grid that fall in the next or previous month
37437
- showDaysInNextAndPreviousMonths: false,
37438
-
37439
- // Allows user to select days that fall in the next or previous month
37440
- enableSelectionDaysInNextAndPreviousMonths: false,
37441
-
37442
- // how many months are visible
37443
- numberOfMonths: 1,
37444
-
37445
- // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`)
37446
- // only used for the first display or when a selected date is not visible
37447
- mainCalendar: 'left',
37448
-
37449
- // Specify a DOM element to render the calendar in
37450
- container: undefined,
37451
-
37452
- // Blur field when date is selected
37453
- blurFieldOnSelect : true,
37454
-
37455
- // internationalization
37456
- i18n: {
37457
- previousMonth : 'Previous Month',
37458
- nextMonth : 'Next Month',
37459
- months : ['January','February','March','April','May','June','July','August','September','October','November','December'],
37460
- weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
37461
- weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
37462
- },
37463
-
37464
- // Theme Classname
37465
- theme: null,
37466
-
37467
- // events array
37468
- events: [],
37469
-
37470
- // callback function
37471
- onSelect: null,
37472
- onOpen: null,
37473
- onClose: null,
37474
- onDraw: null,
37475
-
37476
- // Enable keyboard input
37477
- keyboardInput: true
37478
- },
37479
-
37480
-
37481
- /**
37482
- * templating functions to abstract HTML rendering
37483
- */
37484
- renderDayName = function(opts, day, abbr)
37485
- {
37486
- day += opts.firstDay;
37487
- while (day >= 7) {
37488
- day -= 7;
37489
- }
37490
- return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
37491
- },
37492
-
37493
- renderDay = function(opts)
37494
- {
37495
- var arr = [];
37496
- var ariaSelected = 'false';
37497
- if (opts.isEmpty) {
37498
- if (opts.showDaysInNextAndPreviousMonths) {
37499
- arr.push('is-outside-current-month');
37500
-
37501
- if(!opts.enableSelectionDaysInNextAndPreviousMonths) {
37502
- arr.push('is-selection-disabled');
37503
- }
37504
-
37505
- } else {
37506
- return '<td class="is-empty"></td>';
37507
- }
37508
- }
37509
- if (opts.isDisabled) {
37510
- arr.push('is-disabled');
37511
- }
37512
- if (opts.isToday) {
37513
- arr.push('is-today');
37514
- }
37515
- if (opts.isSelected) {
37516
- arr.push('is-selected');
37517
- ariaSelected = 'true';
37518
- }
37519
- if (opts.hasEvent) {
37520
- arr.push('has-event');
37521
- }
37522
- if (opts.isInRange) {
37523
- arr.push('is-inrange');
37524
- }
37525
- if (opts.isStartRange) {
37526
- arr.push('is-startrange');
37527
- }
37528
- if (opts.isEndRange) {
37529
- arr.push('is-endrange');
37530
- }
37531
- return '<td data-day="' + opts.day + '" class="' + arr.join(' ') + '" aria-selected="' + ariaSelected + '">' +
37532
- '<button tabIndex="-1" class="pika-button pika-day" type="button" ' +
37533
- 'data-pika-year="' + opts.year + '" data-pika-month="' + opts.month + '" data-pika-day="' + opts.day + '">' +
37534
- opts.day +
37535
- '</button>' +
37536
- '</td>';
37537
- },
37538
-
37539
- renderWeek = function (d, m, y) {
37540
- // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified.
37541
- var onejan = new Date(y, 0, 1),
37542
- weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7);
37543
- return '<td class="pika-week">' + weekNum + '</td>';
37544
- },
37545
-
37546
- renderRow = function(days, isRTL, pickWholeWeek, isRowSelected)
37547
- {
37548
- return '<tr class="pika-row' + (pickWholeWeek ? ' pick-whole-week' : '') + (isRowSelected ? ' is-selected' : '') + '">' + (isRTL ? days.reverse() : days).join('') + '</tr>';
37549
- },
37550
-
37551
- renderBody = function(rows)
37552
- {
37553
- return '<tbody>' + rows.join('') + '</tbody>';
37554
- },
37555
-
37556
- renderHead = function(opts)
37557
- {
37558
- var i, arr = [];
37559
- if (opts.showWeekNumber) {
37560
- arr.push('<th></th>');
37561
- }
37562
- for (i = 0; i < 7; i++) {
37563
- arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
37564
- }
37565
- return '<thead><tr>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</tr></thead>';
37566
- },
37567
-
37568
- renderTitle = function(instance, c, year, month, refYear, randId)
37569
- {
37570
- var i, j, arr,
37571
- opts = instance._o,
37572
- isMinYear = year === opts.minYear,
37573
- isMaxYear = year === opts.maxYear,
37574
- html = '<div id="' + randId + '" class="pika-title">',
37575
- monthHtml,
37576
- yearHtml,
37577
- prev = true,
37578
- next = true;
37579
-
37580
- for (arr = [], i = 0; i < 12; i++) {
37581
- arr.push('<option value="' + (year === refYear ? i - c : 12 + i - c) + '"' +
37582
- (i === month ? ' selected="selected"': '') +
37583
- ((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled="disabled"' : '') + '>' +
37584
- opts.i18n.months[i] + '</option>');
37585
- }
37586
-
37587
- monthHtml = '<div class="pika-label">' + opts.i18n.months[month] + '<select aria-label="select month" class="pika-select pika-select-month" tabindex="-1">' + arr.join('') + '</select></div>';
37588
-
37589
- if (isArray(opts.yearRange)) {
37590
- i = opts.yearRange[0];
37591
- j = opts.yearRange[1] + 1;
37592
- } else {
37593
- i = year - opts.yearRange;
37594
- j = 1 + year + opts.yearRange;
37595
- }
37596
-
37597
- for (arr = []; i < j && i <= opts.maxYear; i++) {
37598
- if (i >= opts.minYear) {
37599
- arr.push('<option value="' + i + '"' + (i === year ? ' selected="selected"': '') + '>' + (i) + '</option>');
37600
- }
37601
- }
37602
- yearHtml = '<div class="pika-label">' + year + opts.yearSuffix + '<select aria-label="select year" class="pika-select pika-select-year" tabindex="-1">' + arr.join('') + '</select></div>';
37603
-
37604
- if (opts.showMonthAfterYear) {
37605
- html += yearHtml + monthHtml;
37606
- } else {
37607
- html += monthHtml + yearHtml;
37608
- }
37609
-
37610
- if (isMinYear && (month === 0 || opts.minMonth >= month)) {
37611
- prev = false;
37612
- }
37613
-
37614
- if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
37615
- next = false;
37616
- }
37617
-
37618
- if (c === 0) {
37619
- html += '<button tabIndex="-1" class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
37620
- }
37621
- if (c === (instance._o.numberOfMonths - 1) ) {
37622
- html += '<button tabIndex="-1" class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
37623
- }
37624
-
37625
- return html += '</div>';
37626
- },
37287
+ class TimezonePicker extends ChartComponent {
37288
+ constructor(renderTarget) {
37289
+ super(renderTarget);
37290
+ this.timeZones = ["Local", "UTC", "Africa/Algiers", "Africa/Cairo", "Africa/Casablanca", "Africa/Harare", "Africa/Johannesburg", "Africa/Lagos", "Africa/Nairobi", "Africa/Windhoek", "America/Anchorage", "America/Bogota", "America/Buenos Aires", "America/Caracas", "America/Chicago", "America/Chihuahua", "America/Denver", "America/Edmonton", "America/Godthab", "America/Guatemala", "America/Halifax", "America/Indiana/Indianapolis", "America/Los Angeles", "America/Manaus", "America/Mexico City", "America/Montevideo", "America/New York", "America/Phoenix", "America/Santiago", "America/Sao Paulo", "America/St Johns", "America/Tijuana", "America/Toronto", "America/Vancouver", "America/Winnipeg", "Asia/Amman", "Asia/Beirut", "Asia/Baghdad", "Asia/Baku", "Asia/Bangkok", "Asia/Calcutta", "Asia/Colombo", "Asia/Dhaka", "Asia/Dubai", "Asia/Ho Chi Minh", "Asia/Hong Kong", "Asia/Irkutsk", "Asia/Istanbul", "Asia/Jakarta", "Asia/Jerusalem", "Asia/Kabul", "Asia/Karachi", "Asia/Kathmandu", "Asia/Krasnoyarsk", "Asia/Kuala Lumpur", "Asia/Kuwait", "Asia/Magadan", "Asia/Muscat", "Asia/Novosibirsk", "Asia/Qatar", "Asia/Rangoon", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Taipei", "Asia/Tbilisi", "Asia/Tehran", "Asia/Tokyo", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Cape Verde", "Atlantic/South Georgia", "Australia/Adelaide", "Australia/Brisbane", "Australia/Canberra", "Australia/Darwin", "Australia/Hobart", "Australia/Melbourne", "Australia/Perth", "Australia/Queensland", "Australia/Sydney", "Europe/Amsterdam", "Europe/Andorra", "Europe/Athens", "Europe/Belfast", "Europe/Belgrade", "Europe/Berlin", "Europe/Brussels", "Europe/Budapest", "Europe/Dublin", "Europe/Helsinki", "Europe/Kiev", "Europe/Lisbon", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Oslo", "Europe/Paris", "Europe/Rome", "Europe/Stockholm", "Europe/Vienna", "Europe/Warsaw", "Europe/Zurich", "Pacific/Auckland", "Pacific/Fiji", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Midway", "Pacific/Tongatapu"];
37291
+ }
37292
+ sortTimezones() {
37293
+ let filteredTimezones = this.timeZones.filter((tz) => {
37294
+ return !(tz.toLowerCase() == 'local' || tz == 'UTC');
37295
+ });
37296
+ filteredTimezones.sort((a, b) => {
37297
+ let aOffset = moment$1.tz(new Date(), a.split(' ').join('_')).utcOffset();
37298
+ let bOffset = moment$1.tz(new Date(), b.split(' ').join('_')).utcOffset();
37299
+ if (aOffset < bOffset) {
37300
+ return -1;
37301
+ }
37302
+ if (aOffset > bOffset) {
37303
+ return 1;
37304
+ }
37305
+ return 0;
37306
+ });
37307
+ this.timeZones = ['Local', 'UTC'].concat(filteredTimezones);
37308
+ }
37309
+ render(onTimezoneSelect, defaultTimeZone = null) {
37310
+ this.targetElement = select(this.renderTarget)
37311
+ .classed("tsi-timezonePicker", true);
37312
+ var timezoneSelection = this.targetElement.append("select")
37313
+ .attr("class", "tsi-timezonePicker tsi-select");
37314
+ this.sortTimezones();
37315
+ var options = timezoneSelection.selectAll("option")
37316
+ .data(this.timeZones)
37317
+ .enter()
37318
+ .append("option")
37319
+ .attr('value', d => d)
37320
+ .text((tz) => Utils.convertTimezoneToLabel(tz, this.getString('Local')));
37321
+ timezoneSelection.on("change", function (d) {
37322
+ var timezone = select(this).node().value.replace(/\s/g, "_");
37323
+ onTimezoneSelect(timezone);
37324
+ });
37325
+ defaultTimeZone = defaultTimeZone.replace(/_/g, " ");
37326
+ if (defaultTimeZone != null) {
37327
+ options.filter((d) => d == defaultTimeZone).attr("selected", true);
37328
+ }
37329
+ return;
37330
+ }
37331
+ }
37627
37332
 
37628
- renderTable = function(opts, data, randId)
37629
- {
37630
- return '<table cellpadding="0" cellspacing="0" class="pika-table" role="grid" aria-labelledby="' + randId + '">' + renderHead(opts) + renderBody(data) + '</table>';
37631
- },
37333
+ var momentExports = requireMoment();
37334
+ var moment = /*@__PURE__*/getDefaultExportFromCjs(momentExports);
37632
37335
 
37633
-
37634
- /**
37635
- * Pikaday constructor
37636
- */
37637
- Pikaday = function(options)
37638
- {
37639
- var self = this,
37640
- opts = self.config(options);
37641
-
37642
- self._onMouseDown = function(e)
37643
- {
37644
- if (!self._v) {
37645
- return;
37646
- }
37647
- e = e || window.event;
37648
- var target = e.target || e.srcElement;
37649
- if (!target) {
37650
- return;
37651
- }
37652
-
37653
- if (!hasClass(target, 'is-disabled')) {
37654
- if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) {
37655
- self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day')));
37656
- if (opts.bound) {
37657
- sto(function() {
37658
- self.hide();
37659
- if (opts.blurFieldOnSelect && opts.field) {
37660
- opts.field.blur();
37661
- }
37662
- }, 100);
37663
- }
37664
- }
37665
- else if (hasClass(target, 'pika-prev')) {
37666
- self.prevMonth();
37667
- }
37668
- else if (hasClass(target, 'pika-next')) {
37669
- self.nextMonth();
37670
- }
37671
- }
37672
- if (!hasClass(target, 'pika-select')) {
37673
- // if this is touch event prevent mouse events emulation
37674
- if (e.preventDefault) {
37675
- e.preventDefault();
37676
- } else {
37677
- e.returnValue = false;
37678
- return false;
37679
- }
37680
- } else {
37681
- self._c = true;
37682
- }
37683
- };
37684
-
37685
- self._onChange = function(e)
37686
- {
37687
- e = e || window.event;
37688
- var target = e.target || e.srcElement;
37689
- if (!target) {
37690
- return;
37691
- }
37692
- if (hasClass(target, 'pika-select-month')) {
37693
- self.gotoMonth(target.value);
37694
- }
37695
- else if (hasClass(target, 'pika-select-year')) {
37696
- self.gotoYear(target.value);
37697
- }
37698
- };
37699
-
37700
- self._onKeyChange = function(e)
37701
- {
37702
- e = e || window.event;
37703
- // ignore if event comes from input box
37704
- if (self.isVisible() && e.target && e.target.type !== 'text') {
37705
-
37706
- switch(e.keyCode){
37707
- case 13:
37708
- case 27:
37709
- if (opts.field) {
37710
- opts.field.blur();
37711
- }
37712
- break;
37713
- case 37:
37714
- e.preventDefault();
37715
- self.adjustDate('subtract', 1);
37716
- break;
37717
- case 38:
37718
- self.adjustDate('subtract', 7);
37719
- break;
37720
- case 39:
37721
- self.adjustDate('add', 1);
37722
- break;
37723
- case 40:
37724
- self.adjustDate('add', 7);
37725
- break;
37726
- }
37727
- }
37728
- };
37729
-
37730
- self._onInputChange = function(e)
37731
- {
37732
- var date;
37733
-
37734
- if (e.firedBy === self) {
37735
- return;
37736
- }
37737
- if (opts.parse) {
37738
- date = opts.parse(opts.field.value, opts.format);
37739
- } else if (hasMoment) {
37740
- date = moment(opts.field.value, opts.format, opts.formatStrict);
37741
- date = (date && date.isValid()) ? date.toDate() : null;
37742
- }
37743
- else {
37744
- date = new Date(Date.parse(opts.field.value));
37745
- }
37746
- // if (isDate(date)) {
37747
- // self.setDate(date);
37748
- // }
37749
- // if (!self._v) {
37750
- // self.show();
37751
- // }
37752
- };
37753
-
37754
- self._onInputFocus = function()
37755
- {
37756
- self.show();
37757
- };
37758
-
37759
- self._onInputClick = function()
37760
- {
37761
- self.show();
37762
- };
37763
-
37764
- self._onInputBlur = function()
37765
- {
37766
- // IE allows pika div to gain focus; catch blur the input field
37767
- var pEl = document.activeElement;
37768
- do {
37769
- if (hasClass(pEl, 'pika-single')) {
37770
- return;
37771
- }
37772
- }
37773
- while ((pEl = pEl.parentNode));
37774
-
37775
- if (!self._c) {
37776
- self._b = sto(function() {
37777
- self.hide();
37778
- }, 50);
37779
- }
37780
- self._c = false;
37781
- };
37782
-
37783
- self._onClick = function(e)
37784
- {
37785
- e = e || window.event;
37786
- var target = e.target || e.srcElement,
37787
- pEl = target;
37788
- if (!target) {
37789
- return;
37790
- }
37791
- if (!hasEventListeners && hasClass(target, 'pika-select')) {
37792
- if (!target.onchange) {
37793
- target.setAttribute('onchange', 'return;');
37794
- addEvent(target, 'change', self._onChange);
37795
- }
37796
- }
37797
- do {
37798
- if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) {
37799
- return;
37800
- }
37801
- }
37802
- while ((pEl = pEl.parentNode));
37803
- if (self._v && target !== opts.trigger && pEl !== opts.trigger) {
37804
- self.hide();
37805
- }
37806
- };
37807
-
37808
- self.el = document.createElement('div');
37809
- self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : '');
37810
-
37811
- addEvent(self.el, 'mousedown', self._onMouseDown, true);
37812
- addEvent(self.el, 'touchend', self._onMouseDown, true);
37813
- addEvent(self.el, 'change', self._onChange);
37814
-
37815
- if (opts.keyboardInput) {
37816
- addEvent(document, 'keydown', self._onKeyChange);
37817
- }
37818
-
37819
- if (opts.field) {
37820
- if (opts.container) {
37821
- opts.container.appendChild(self.el);
37822
- } else if (opts.bound) {
37823
- document.body.appendChild(self.el);
37824
- } else {
37825
- opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
37826
- }
37827
- addEvent(opts.field, 'change', self._onInputChange);
37828
-
37829
- if (!opts.defaultDate) {
37830
- if (hasMoment && opts.field.value) {
37831
- opts.defaultDate = moment(opts.field.value, opts.format).toDate();
37832
- } else {
37833
- opts.defaultDate = new Date(Date.parse(opts.field.value));
37834
- }
37835
- opts.setDefaultDate = true;
37836
- }
37837
- }
37838
-
37839
- var defDate = opts.defaultDate;
37840
-
37841
- if (isDate(defDate)) {
37842
- if (opts.setDefaultDate) {
37843
- self.setDate(defDate, true);
37844
- } else {
37845
- self.gotoDate(defDate);
37846
- }
37847
- } else {
37848
- self.gotoDate(new Date());
37849
- }
37850
-
37851
- if (opts.bound) {
37852
- this.hide();
37853
- self.el.className += ' is-bound';
37854
- addEvent(opts.trigger, 'click', self._onInputClick);
37855
- addEvent(opts.trigger, 'focus', self._onInputFocus);
37856
- addEvent(opts.trigger, 'blur', self._onInputBlur);
37857
- } else {
37858
- this.show();
37859
- }
37860
- };
37861
-
37862
-
37863
- /**
37864
- * public Pikaday API
37865
- */
37866
- Pikaday.prototype = {
37867
-
37868
-
37869
- /**
37870
- * configure functionality
37871
- */
37872
- config: function(options)
37873
- {
37874
- if (!this._o) {
37875
- this._o = extend({}, defaults, true);
37876
- }
37877
-
37878
- var opts = extend(this._o, options, true);
37879
-
37880
- opts.isRTL = !!opts.isRTL;
37881
-
37882
- opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
37883
-
37884
- opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null;
37885
-
37886
- opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
37887
-
37888
- opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field;
37889
-
37890
- opts.disableWeekends = !!opts.disableWeekends;
37891
-
37892
- opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null;
37893
-
37894
- var nom = parseInt(opts.numberOfMonths, 10) || 1;
37895
- opts.numberOfMonths = nom > 4 ? 4 : nom;
37896
-
37897
- if (!isDate(opts.minDate)) {
37898
- opts.minDate = false;
37899
- }
37900
- if (!isDate(opts.maxDate)) {
37901
- opts.maxDate = false;
37902
- }
37903
- if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
37904
- opts.maxDate = opts.minDate = false;
37905
- }
37906
- if (opts.minDate) {
37907
- this.setMinDate(opts.minDate);
37908
- }
37909
- if (opts.maxDate) {
37910
- this.setMaxDate(opts.maxDate);
37911
- }
37912
-
37913
- if (isArray(opts.yearRange)) {
37914
- var fallback = new Date().getFullYear() - 10;
37915
- opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
37916
- opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
37917
- } else {
37918
- opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
37919
- if (opts.yearRange > 100) {
37920
- opts.yearRange = 100;
37921
- }
37922
- }
37923
-
37924
- return opts;
37925
- },
37926
-
37927
- /**
37928
- * return a formatted string of the current selection (using Moment.js if available)
37929
- */
37930
- toString: function(format)
37931
- {
37932
- format = format || this._o.format;
37933
- if (!isDate(this._d)) {
37934
- return '';
37935
- }
37936
- if (this._o.toString) {
37937
- return this._o.toString(this._d, format);
37938
- }
37939
- if (hasMoment) {
37940
- return moment(this._d).format(format);
37941
- }
37942
- return this._d.toDateString();
37943
- },
37944
-
37945
- /**
37946
- * return a Moment.js object of the current selection (if available)
37947
- */
37948
- getMoment: function()
37949
- {
37950
- return hasMoment ? moment(this._d) : null;
37951
- },
37952
-
37953
- /**
37954
- * set the current selection from a Moment.js object (if available)
37955
- */
37956
- setMoment: function(date, preventOnSelect)
37957
- {
37958
- if (hasMoment && moment.isMoment(date)) {
37959
- this.setDate(date.toDate(), preventOnSelect);
37960
- }
37961
- },
37962
-
37963
- /**
37964
- * return a Date object of the current selection
37965
- */
37966
- getDate: function()
37967
- {
37968
- return isDate(this._d) ? new Date(this._d.getTime()) : null;
37969
- },
37970
-
37971
- /**
37972
- * set the current selection
37973
- */
37974
- setDate: function(date, preventOnSelect)
37975
- {
37976
- if (!date) {
37977
- this._d = null;
37978
-
37979
- if (this._o.field) {
37980
- this._o.field.value = '';
37981
- fireEvent(this._o.field, 'change', { firedBy: this });
37982
- }
37983
-
37984
- return this.draw();
37985
- }
37986
- if (typeof date === 'string') {
37987
- date = new Date(Date.parse(date));
37988
- }
37989
- if (!isDate(date)) {
37990
- return;
37991
- }
37992
-
37993
- var min = this._o.minDate,
37994
- max = this._o.maxDate;
37995
-
37996
- if (isDate(min) && date < min) {
37997
- date = min;
37998
- } else if (isDate(max) && date > max) {
37999
- date = max;
38000
- }
38001
-
38002
- this._d = new Date(date.getTime());
38003
- setToStartOfDay(this._d);
38004
- this.gotoDate(this._d);
38005
-
38006
- if (this._o.field) {
38007
- this._o.field.value = this.toString();
38008
- fireEvent(this._o.field, 'change', { firedBy: this });
38009
- }
38010
- if (!preventOnSelect && typeof this._o.onSelect === 'function') {
38011
- this._o.onSelect.call(this, this.getDate());
38012
- }
38013
- },
38014
-
38015
- /**
38016
- * change view to a specific date
38017
- */
38018
- gotoDate: function(date)
38019
- {
38020
- var newCalendar = true;
38021
-
38022
- if (!isDate(date)) {
38023
- return;
38024
- }
38025
-
38026
- if (this.calendars) {
38027
- var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1),
38028
- lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1),
38029
- visibleDate = date.getTime();
38030
- // get the end of the month
38031
- lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1);
38032
- lastVisibleDate.setDate(lastVisibleDate.getDate()-1);
38033
- newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate);
38034
- }
38035
-
38036
- if (newCalendar) {
38037
- this.calendars = [{
38038
- month: date.getMonth(),
38039
- year: date.getFullYear()
38040
- }];
38041
- if (this._o.mainCalendar === 'right') {
38042
- this.calendars[0].month += 1 - this._o.numberOfMonths;
38043
- }
38044
- }
38045
-
38046
- this.adjustCalendars();
38047
- },
38048
-
38049
- adjustDate: function(sign, days) {
38050
-
38051
- var day = this.getDate() || new Date();
38052
- var difference = parseInt(days)*24*60*60*1000;
38053
-
38054
- var newDay;
38055
-
38056
- if (sign === 'add') {
38057
- newDay = new Date(day.valueOf() + difference);
38058
- } else if (sign === 'subtract') {
38059
- newDay = new Date(day.valueOf() - difference);
38060
- }
38061
-
38062
- this.setDate(newDay);
38063
- },
38064
-
38065
- adjustCalendars: function() {
38066
- this.calendars[0] = adjustCalendar(this.calendars[0]);
38067
- for (var c = 1; c < this._o.numberOfMonths; c++) {
38068
- this.calendars[c] = adjustCalendar({
38069
- month: this.calendars[0].month + c,
38070
- year: this.calendars[0].year
38071
- });
38072
- }
38073
- this.draw();
38074
- },
38075
-
38076
- gotoToday: function()
38077
- {
38078
- this.gotoDate(new Date());
38079
- },
38080
-
38081
- /**
38082
- * change view to a specific month (zero-index, e.g. 0: January)
38083
- */
38084
- gotoMonth: function(month)
38085
- {
38086
- if (!isNaN(month)) {
38087
- this.calendars[0].month = parseInt(month, 10);
38088
- this.adjustCalendars();
38089
- }
38090
- },
38091
-
38092
- nextMonth: function()
38093
- {
38094
- this.calendars[0].month++;
38095
- this.adjustCalendars();
38096
- },
38097
-
38098
- prevMonth: function()
38099
- {
38100
- this.calendars[0].month--;
38101
- this.adjustCalendars();
38102
- },
38103
-
38104
- /**
38105
- * change view to a specific full year (e.g. "2012")
38106
- */
38107
- gotoYear: function(year)
38108
- {
38109
- if (!isNaN(year)) {
38110
- this.calendars[0].year = parseInt(year, 10);
38111
- this.adjustCalendars();
38112
- }
38113
- },
38114
-
38115
- /**
38116
- * change the minDate
38117
- */
38118
- setMinDate: function(value)
38119
- {
38120
- if(value instanceof Date) {
38121
- setToStartOfDay(value);
38122
- this._o.minDate = value;
38123
- this._o.minYear = value.getFullYear();
38124
- this._o.minMonth = value.getMonth();
38125
- } else {
38126
- this._o.minDate = defaults.minDate;
38127
- this._o.minYear = defaults.minYear;
38128
- this._o.minMonth = defaults.minMonth;
38129
- this._o.startRange = defaults.startRange;
38130
- }
38131
-
38132
- this.draw();
38133
- },
38134
-
38135
- /**
38136
- * change the maxDate
38137
- */
38138
- setMaxDate: function(value)
38139
- {
38140
- if(value instanceof Date) {
38141
- setToStartOfDay(value);
38142
- this._o.maxDate = value;
38143
- this._o.maxYear = value.getFullYear();
38144
- this._o.maxMonth = value.getMonth();
38145
- } else {
38146
- this._o.maxDate = defaults.maxDate;
38147
- this._o.maxYear = defaults.maxYear;
38148
- this._o.maxMonth = defaults.maxMonth;
38149
- this._o.endRange = defaults.endRange;
38150
- }
38151
-
38152
- this.draw();
38153
- },
38154
-
38155
- setStartRange: function(value)
38156
- {
38157
- this._o.startRange = value;
38158
- },
38159
-
38160
- setEndRange: function(value)
38161
- {
38162
- this._o.endRange = value;
38163
- },
38164
-
38165
- /**
38166
- * refresh the HTML
38167
- */
38168
- draw: function(force)
38169
- {
38170
- if (!this._v && !force) {
38171
- return;
38172
- }
38173
- var opts = this._o,
38174
- minYear = opts.minYear,
38175
- maxYear = opts.maxYear,
38176
- minMonth = opts.minMonth,
38177
- maxMonth = opts.maxMonth,
38178
- html = '',
38179
- randId;
38180
-
38181
- if (this._y <= minYear) {
38182
- this._y = minYear;
38183
- if (!isNaN(minMonth) && this._m < minMonth) {
38184
- this._m = minMonth;
38185
- }
38186
- }
38187
- if (this._y >= maxYear) {
38188
- this._y = maxYear;
38189
- if (!isNaN(maxMonth) && this._m > maxMonth) {
38190
- this._m = maxMonth;
38191
- }
38192
- }
38193
-
38194
- for (var c = 0; c < opts.numberOfMonths; c++) {
38195
- randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2);
38196
- html += '<div class="pika-lendar">' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId) + '</div>';
38197
- }
38198
-
38199
- this.el.innerHTML = html;
38200
-
38201
- if (opts.bound) {
38202
- if(opts.field.type !== 'hidden') {
38203
- sto(function() {
38204
- opts.trigger.focus();
38205
- }, 1);
38206
- }
38207
- }
38208
-
38209
- if (typeof this._o.onDraw === 'function') {
38210
- this._o.onDraw(this);
38211
- }
38212
-
38213
- if (opts.bound) {
38214
- // let the screen reader user know to use arrow keys
38215
- opts.field.setAttribute('aria-label', 'Use the arrow keys to pick a date');
38216
- }
38217
- },
38218
-
38219
- adjustPosition: function()
38220
- {
38221
- var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect;
38222
-
38223
- if (this._o.container) return;
38224
-
38225
- this.el.style.position = 'absolute';
38226
-
38227
- field = this._o.trigger;
38228
- pEl = field;
38229
- width = this.el.offsetWidth;
38230
- height = this.el.offsetHeight;
38231
- viewportWidth = window.innerWidth || document.documentElement.clientWidth;
38232
- viewportHeight = window.innerHeight || document.documentElement.clientHeight;
38233
- scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
38234
-
38235
- if (typeof field.getBoundingClientRect === 'function') {
38236
- clientRect = field.getBoundingClientRect();
38237
- left = clientRect.left + window.pageXOffset;
38238
- top = clientRect.bottom + window.pageYOffset;
38239
- } else {
38240
- left = pEl.offsetLeft;
38241
- top = pEl.offsetTop + pEl.offsetHeight;
38242
- while((pEl = pEl.offsetParent)) {
38243
- left += pEl.offsetLeft;
38244
- top += pEl.offsetTop;
38245
- }
38246
- }
38247
-
38248
- // default position is bottom & left
38249
- if ((this._o.reposition && left + width > viewportWidth) ||
38250
- (
38251
- this._o.position.indexOf('right') > -1 &&
38252
- left - width + field.offsetWidth > 0
38253
- )
38254
- ) {
38255
- left = left - width + field.offsetWidth;
38256
- }
38257
- if ((this._o.reposition && top + height > viewportHeight + scrollTop) ||
38258
- (
38259
- this._o.position.indexOf('top') > -1 &&
38260
- top - height - field.offsetHeight > 0
38261
- )
38262
- ) {
38263
- top = top - height - field.offsetHeight;
38264
- }
38265
-
38266
- this.el.style.left = left + 'px';
38267
- this.el.style.top = top + 'px';
38268
- },
38269
-
38270
- /**
38271
- * render HTML for a particular month
38272
- */
38273
- render: function(year, month, randId)
38274
- {
38275
- var opts = this._o,
38276
- now = new Date(),
38277
- days = getDaysInMonth(year, month),
38278
- before = new Date(year, month, 1).getDay(),
38279
- data = [],
38280
- row = [];
38281
- setToStartOfDay(now);
38282
- if (opts.firstDay > 0) {
38283
- before -= opts.firstDay;
38284
- if (before < 0) {
38285
- before += 7;
38286
- }
38287
- }
38288
- var previousMonth = month === 0 ? 11 : month - 1,
38289
- nextMonth = month === 11 ? 0 : month + 1,
38290
- yearOfPreviousMonth = month === 0 ? year - 1 : year,
38291
- yearOfNextMonth = month === 11 ? year + 1 : year,
38292
- daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth);
38293
- var cells = days + before,
38294
- after = cells;
38295
- while(after > 7) {
38296
- after -= 7;
38297
- }
38298
- cells += 7 - after;
38299
- var isWeekSelected = false;
38300
- for (var i = 0, r = 0; i < cells; i++)
38301
- {
38302
- var day = new Date(year, month, 1 + (i - before)),
38303
- isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
38304
- isToday = compareDates(day, now),
38305
- hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false,
38306
- isEmpty = i < before || i >= (days + before),
38307
- dayNumber = 1 + (i - before),
38308
- monthNumber = month,
38309
- yearNumber = year,
38310
- isStartRange = opts.startRange && compareDates(opts.startRange, day),
38311
- isEndRange = opts.endRange && compareDates(opts.endRange, day),
38312
- isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange,
38313
- isDisabled = (opts.minDate && day < opts.minDate) ||
38314
- (opts.maxDate && day > opts.maxDate) ||
38315
- (opts.disableWeekends && isWeekend(day)) ||
38316
- (opts.disableDayFn && opts.disableDayFn(day));
38317
-
38318
- if (isEmpty) {
38319
- if (i < before) {
38320
- dayNumber = daysInPreviousMonth + dayNumber;
38321
- monthNumber = previousMonth;
38322
- yearNumber = yearOfPreviousMonth;
38323
- } else {
38324
- dayNumber = dayNumber - days;
38325
- monthNumber = nextMonth;
38326
- yearNumber = yearOfNextMonth;
38327
- }
38328
- }
38329
-
38330
- var dayConfig = {
38331
- day: dayNumber,
38332
- month: monthNumber,
38333
- year: yearNumber,
38334
- hasEvent: hasEvent,
38335
- isSelected: isSelected,
38336
- isToday: isToday,
38337
- isDisabled: isDisabled,
38338
- isEmpty: isEmpty,
38339
- isStartRange: isStartRange,
38340
- isEndRange: isEndRange,
38341
- isInRange: isInRange,
38342
- showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths,
38343
- enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths
38344
- };
38345
-
38346
- if (opts.pickWholeWeek && isSelected) {
38347
- isWeekSelected = true;
38348
- }
38349
-
38350
- row.push(renderDay(dayConfig));
38351
-
38352
- if (++r === 7) {
38353
- if (opts.showWeekNumber) {
38354
- row.unshift(renderWeek(i - before, month, year));
38355
- }
38356
- data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected));
38357
- row = [];
38358
- r = 0;
38359
- isWeekSelected = false;
38360
- }
38361
- }
38362
- return renderTable(opts, data, randId);
38363
- },
38364
-
38365
- isVisible: function()
38366
- {
38367
- return this._v;
38368
- },
38369
-
38370
- show: function()
38371
- {
38372
- if (!this.isVisible()) {
38373
- this._v = true;
38374
- this.draw();
38375
- removeClass(this.el, 'is-hidden');
38376
- if (this._o.bound) {
38377
- addEvent(document, 'click', this._onClick);
38378
- this.adjustPosition();
38379
- }
38380
- if (typeof this._o.onOpen === 'function') {
38381
- this._o.onOpen.call(this);
38382
- }
38383
- }
38384
- },
38385
-
38386
- hide: function()
38387
- {
38388
- var v = this._v;
38389
- if (v !== false) {
38390
- if (this._o.bound) {
38391
- removeEvent(document, 'click', this._onClick);
38392
- }
38393
- this.el.style.position = 'static'; // reset
38394
- this.el.style.left = 'auto';
38395
- this.el.style.top = 'auto';
38396
- addClass(this.el, 'is-hidden');
38397
- this._v = false;
38398
- if (v !== undefined && typeof this._o.onClose === 'function') {
38399
- this._o.onClose.call(this);
38400
- }
38401
- }
38402
- },
38403
-
38404
- /**
38405
- * GAME OVER
38406
- */
38407
- destroy: function()
38408
- {
38409
- var opts = this._o;
38410
-
38411
- this.hide();
38412
- removeEvent(this.el, 'mousedown', this._onMouseDown, true);
38413
- removeEvent(this.el, 'touchend', this._onMouseDown, true);
38414
- removeEvent(this.el, 'change', this._onChange);
38415
- if (opts.keyboardInput) {
38416
- removeEvent(document, 'keydown', this._onKeyChange);
38417
- }
38418
- if (opts.field) {
38419
- removeEvent(opts.field, 'change', this._onInputChange);
38420
- if (opts.bound) {
38421
- removeEvent(opts.trigger, 'click', this._onInputClick);
38422
- removeEvent(opts.trigger, 'focus', this._onInputFocus);
38423
- removeEvent(opts.trigger, 'blur', this._onInputBlur);
38424
- }
38425
- }
38426
- if (this.el.parentNode) {
38427
- this.el.parentNode.removeChild(this.el);
38428
- }
38429
- }
38430
-
38431
- };
38432
-
38433
- return Pikaday;
38434
- }));
38435
- } (pikaday$1));
38436
- return pikaday$1.exports;
38437
- }
38438
-
38439
- var pikadayExports = /*@__PURE__*/ requirePikaday();
38440
- var Pikaday = /*@__PURE__*/getDefaultExportFromCjs(pikadayExports);
38441
-
38442
- var momentExports = requireMoment();
38443
- var moment = /*@__PURE__*/getDefaultExportFromCjs(momentExports);
38444
-
38445
- class TimezonePicker extends ChartComponent {
38446
- constructor(renderTarget) {
38447
- super(renderTarget);
38448
- this.timeZones = ["Local", "UTC", "Africa/Algiers", "Africa/Cairo", "Africa/Casablanca", "Africa/Harare", "Africa/Johannesburg", "Africa/Lagos", "Africa/Nairobi", "Africa/Windhoek", "America/Anchorage", "America/Bogota", "America/Buenos Aires", "America/Caracas", "America/Chicago", "America/Chihuahua", "America/Denver", "America/Edmonton", "America/Godthab", "America/Guatemala", "America/Halifax", "America/Indiana/Indianapolis", "America/Los Angeles", "America/Manaus", "America/Mexico City", "America/Montevideo", "America/New York", "America/Phoenix", "America/Santiago", "America/Sao Paulo", "America/St Johns", "America/Tijuana", "America/Toronto", "America/Vancouver", "America/Winnipeg", "Asia/Amman", "Asia/Beirut", "Asia/Baghdad", "Asia/Baku", "Asia/Bangkok", "Asia/Calcutta", "Asia/Colombo", "Asia/Dhaka", "Asia/Dubai", "Asia/Ho Chi Minh", "Asia/Hong Kong", "Asia/Irkutsk", "Asia/Istanbul", "Asia/Jakarta", "Asia/Jerusalem", "Asia/Kabul", "Asia/Karachi", "Asia/Kathmandu", "Asia/Krasnoyarsk", "Asia/Kuala Lumpur", "Asia/Kuwait", "Asia/Magadan", "Asia/Muscat", "Asia/Novosibirsk", "Asia/Qatar", "Asia/Rangoon", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Taipei", "Asia/Tbilisi", "Asia/Tehran", "Asia/Tokyo", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Cape Verde", "Atlantic/South Georgia", "Australia/Adelaide", "Australia/Brisbane", "Australia/Canberra", "Australia/Darwin", "Australia/Hobart", "Australia/Melbourne", "Australia/Perth", "Australia/Queensland", "Australia/Sydney", "Europe/Amsterdam", "Europe/Andorra", "Europe/Athens", "Europe/Belfast", "Europe/Belgrade", "Europe/Berlin", "Europe/Brussels", "Europe/Budapest", "Europe/Dublin", "Europe/Helsinki", "Europe/Kiev", "Europe/Lisbon", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Oslo", "Europe/Paris", "Europe/Rome", "Europe/Stockholm", "Europe/Vienna", "Europe/Warsaw", "Europe/Zurich", "Pacific/Auckland", "Pacific/Fiji", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Midway", "Pacific/Tongatapu"];
38449
- }
38450
- sortTimezones() {
38451
- let filteredTimezones = this.timeZones.filter((tz) => {
38452
- return !(tz.toLowerCase() == 'local' || tz == 'UTC');
38453
- });
38454
- filteredTimezones.sort((a, b) => {
38455
- let aOffset = moment$1.tz(new Date(), a.split(' ').join('_')).utcOffset();
38456
- let bOffset = moment$1.tz(new Date(), b.split(' ').join('_')).utcOffset();
38457
- if (aOffset < bOffset) {
38458
- return -1;
38459
- }
38460
- if (aOffset > bOffset) {
38461
- return 1;
38462
- }
38463
- return 0;
38464
- });
38465
- this.timeZones = ['Local', 'UTC'].concat(filteredTimezones);
38466
- }
38467
- render(onTimezoneSelect, defaultTimeZone = null) {
38468
- this.targetElement = select(this.renderTarget)
38469
- .classed("tsi-timezonePicker", true);
38470
- var timezoneSelection = this.targetElement.append("select")
38471
- .attr("class", "tsi-timezonePicker tsi-select");
38472
- this.sortTimezones();
38473
- var options = timezoneSelection.selectAll("option")
38474
- .data(this.timeZones)
38475
- .enter()
38476
- .append("option")
38477
- .attr('value', d => d)
38478
- .text((tz) => Utils.convertTimezoneToLabel(tz, this.getString('Local')));
38479
- timezoneSelection.on("change", function (d) {
38480
- var timezone = select(this).node().value.replace(/\s/g, "_");
38481
- onTimezoneSelect(timezone);
38482
- });
38483
- defaultTimeZone = defaultTimeZone.replace(/_/g, " ");
38484
- if (defaultTimeZone != null) {
38485
- options.filter((d) => d == defaultTimeZone).attr("selected", true);
38486
- }
38487
- return;
38488
- }
38489
- }
37336
+ // Ensure moment is available globally for Pikaday
37337
+ if (typeof window !== 'undefined') {
37338
+ window.moment = moment;
37339
+ }
37340
+ // Export a function to safely create Pikaday instances
37341
+ function createPikaday(options) {
37342
+ if (typeof window === 'undefined') {
37343
+ console.warn('Pikaday requires a browser environment');
37344
+ return null;
37345
+ }
37346
+ const Pikaday = window.Pikaday;
37347
+ if (!Pikaday) {
37348
+ console.error('Pikaday not available. Make sure pikaday.js is loaded.');
37349
+ return null;
37350
+ }
37351
+ if (!moment || !window.moment) {
37352
+ console.error('Moment.js not available. Pikaday requires moment.js.');
37353
+ return null;
37354
+ }
37355
+ return new Pikaday(options);
37356
+ }
38490
37357
 
38491
37358
  class DateTimePicker extends ChartComponent {
38492
37359
  constructor(renderTarget) {
@@ -38758,8 +37625,8 @@
38758
37625
  weekdays: moment.localeData().weekdays(),
38759
37626
  weekdaysShort: moment.localeData().weekdaysMin()
38760
37627
  };
38761
- //@ts-ignore
38762
- this.calendarPicker = new Pikaday({
37628
+ // Use the safe Pikaday wrapper
37629
+ this.calendarPicker = createPikaday({
38763
37630
  bound: false,
38764
37631
  container: this.calendar.node(),
38765
37632
  field: this.calendar.node(),
@@ -38795,6 +37662,11 @@
38795
37662
  maxDate: this.convertToCalendarDate(this.maxMillis),
38796
37663
  defaultDate: Utils.adjustDateFromTimezoneOffset(new Date(this.fromMillis))
38797
37664
  });
37665
+ // Check if Pikaday was created successfully
37666
+ if (!this.calendarPicker) {
37667
+ console.error('Failed to create Pikaday calendar. Check moment.js availability.');
37668
+ return;
37669
+ }
38798
37670
  }
38799
37671
  setSelectedQuickTimes() {
38800
37672
  let isSelected = d => {
@@ -39055,7 +37927,30 @@
39055
37927
  this.pickerIsVisible = false;
39056
37928
  }
39057
37929
  buttonDateTimeFormat(millis) {
39058
- return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
37930
+ const date = new Date(millis);
37931
+ const locale = this.chartOptions.dateLocale || 'en-US';
37932
+ const is24Hour = this.chartOptions.is24HourTime !== false;
37933
+ const formatOptions = {
37934
+ year: 'numeric',
37935
+ month: '2-digit',
37936
+ day: '2-digit',
37937
+ hour: '2-digit',
37938
+ minute: '2-digit',
37939
+ second: '2-digit',
37940
+ hour12: !is24Hour
37941
+ };
37942
+ try {
37943
+ if (this.chartOptions.offset && this.chartOptions.offset !== 'Local') {
37944
+ formatOptions.timeZone = this.getTimezoneFromOffset(this.chartOptions.offset);
37945
+ }
37946
+ const baseFormat = date.toLocaleString(locale, formatOptions);
37947
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
37948
+ return `${baseFormat}.${milliseconds}`;
37949
+ }
37950
+ catch (error) {
37951
+ console.warn(`Failed to format date for locale ${locale}:`, error);
37952
+ return Utils.timeFormat(!this.chartOptions.minutesForTimeLabels, !this.chartOptions.minutesForTimeLabels, this.chartOptions.offset, this.chartOptions.is24HourTime, 0, null, this.chartOptions.dateLocale)(millis);
37953
+ }
39059
37954
  }
39060
37955
  render(chartOptions, minMillis, maxMillis, onSet = null) {
39061
37956
  this.chartOptions.setOptions(chartOptions);
@@ -39075,11 +37970,22 @@
39075
37970
  }
39076
37971
  super.themify(select(this.renderTarget), this.chartOptions.theme);
39077
37972
  }
37973
+ getTimezoneFromOffset(offset) {
37974
+ const timezoneMap = {
37975
+ 'UTC': 'UTC',
37976
+ 'EST': 'America/New_York',
37977
+ 'PST': 'America/Los_Angeles',
37978
+ 'CST': 'America/Chicago',
37979
+ 'MST': 'America/Denver'
37980
+ };
37981
+ return timezoneMap[offset] || 'UTC';
37982
+ }
39078
37983
  }
39079
37984
 
39080
37985
  class DateTimeButtonRange extends DateTimeButton {
39081
37986
  constructor(renderTarget) {
39082
37987
  super(renderTarget);
37988
+ this.clickOutsideHandler = null;
39083
37989
  }
39084
37990
  setButtonText(fromMillis, toMillis, isRelative, quickTime) {
39085
37991
  let fromString = this.buttonDateTimeFormat(fromMillis);
@@ -39099,10 +38005,38 @@
39099
38005
  onClose() {
39100
38006
  this.dateTimePickerContainer.style("display", "none");
39101
38007
  this.dateTimeButton.node().focus();
38008
+ this.removeClickOutsideHandler();
38009
+ }
38010
+ removeClickOutsideHandler() {
38011
+ if (this.clickOutsideHandler) {
38012
+ document.removeEventListener('click', this.clickOutsideHandler);
38013
+ this.clickOutsideHandler = null;
38014
+ }
38015
+ }
38016
+ setupClickOutsideHandler() {
38017
+ // Remove any existing handler first
38018
+ this.removeClickOutsideHandler();
38019
+ // Add handler after a small delay to prevent the opening click from immediately closing the picker
38020
+ setTimeout(() => {
38021
+ this.clickOutsideHandler = (event) => {
38022
+ const pickerElement = this.dateTimePickerContainer.node();
38023
+ const buttonElement = this.dateTimeButton.node();
38024
+ const target = event.target;
38025
+ // Check if click is outside both the picker and the button
38026
+ if (pickerElement && buttonElement &&
38027
+ !pickerElement.contains(target) &&
38028
+ !buttonElement.contains(target)) {
38029
+ this.onClose();
38030
+ }
38031
+ };
38032
+ document.addEventListener('click', this.clickOutsideHandler);
38033
+ }, 0);
39102
38034
  }
39103
38035
  render(chartOptions = {}, minMillis, maxMillis, fromMillis = null, toMillis = null, onSet = null, onCancel = null) {
39104
38036
  super.render(chartOptions, minMillis, maxMillis, onSet);
39105
- select(this.renderTarget).classed('tsi-dateTimeContainerRange', true);
38037
+ let container = select(this.renderTarget);
38038
+ container.classed('tsi-dateTimeContainerRange', true);
38039
+ container.style('position', 'relative');
39106
38040
  this.fromMillis = fromMillis;
39107
38041
  this.toMillis = toMillis;
39108
38042
  this.onCancel = onCancel ? onCancel : () => { };
@@ -39128,6 +38062,7 @@
39128
38062
  this.onClose();
39129
38063
  this.onCancel();
39130
38064
  });
38065
+ this.setupClickOutsideHandler();
39131
38066
  }
39132
38067
  });
39133
38068
  }
@@ -39203,6 +38138,7 @@
39203
38138
  }
39204
38139
  //transformation of buckets created by the UX client to buckets for the availabilityChart
39205
38140
  createDisplayBuckets(fromMillis, toMillis) {
38141
+ //TODO: "" key is confusing, should be "count" or something similar
39206
38142
  var keysInRange = Object.keys(this.transformedAvailability[0].availabilityCount[""]).reduce((inRangeObj, timestamp, i, timestamps) => {
39207
38143
  var currTSMillis = (new Date(timestamp)).valueOf();
39208
38144
  var nextTSMillis = currTSMillis + this.bucketSize;
@@ -39255,6 +38191,7 @@
39255
38191
  this.bucketSize = null;
39256
38192
  }
39257
38193
  }
38194
+ //TODO: should have proper types for parameters
39258
38195
  render(transformedAvailability, chartOptions, rawAvailability = {}) {
39259
38196
  this.setChartOptions(chartOptions);
39260
38197
  this.rawAvailability = rawAvailability;
@@ -43431,7 +42368,7 @@
43431
42368
  super(renderTarget);
43432
42369
  this.chartOptions = new ChartOptions(); // TODO handle onkeyup and oninput in chart options
43433
42370
  }
43434
- render(environmentFqdn, getToken, chartOptions) {
42371
+ render(chartOptions) {
43435
42372
  this.chartOptions.setOptions(chartOptions);
43436
42373
  let targetElement = select(this.renderTarget);
43437
42374
  targetElement.html("");
@@ -43785,20 +42722,102 @@
43785
42722
  }
43786
42723
  }
43787
42724
 
42725
+ // Centralized renderer for the hierarchy tree. Keeps a stable D3 data-join and
42726
+ // updates existing DOM nodes instead of fully recreating them on each render.
42727
+ class TreeRenderer {
42728
+ static render(owner, data, target) {
42729
+ // Ensure an <ul> exists for this target (one list per level)
42730
+ let list = target.select('ul');
42731
+ if (list.empty()) {
42732
+ list = target.append('ul').attr('role', target === owner.hierarchyElem ? 'tree' : 'group');
42733
+ }
42734
+ const entries = Object.keys(data).map(k => ({ key: k, item: data[k] }));
42735
+ const liSelection = list.selectAll('li').data(entries, (d) => d && d.key);
42736
+ liSelection.exit().remove();
42737
+ const liEnter = liSelection.enter().append('li')
42738
+ .attr('role', 'none')
42739
+ .classed('tsi-leaf', (d) => !!d.item.isLeaf);
42740
+ const liMerged = liEnter.merge(liSelection);
42741
+ const setSize = entries.length;
42742
+ liMerged.each((d, i, nodes) => {
42743
+ const entry = d;
42744
+ const li = select(nodes[i]);
42745
+ if (owner.selectedIds && owner.selectedIds.includes(entry.item.id)) {
42746
+ li.classed('tsi-selected', true);
42747
+ }
42748
+ else {
42749
+ li.classed('tsi-selected', false);
42750
+ }
42751
+ // determine instance vs hierarchy node by presence of isLeaf flag
42752
+ const isInstance = !!entry.item.isLeaf;
42753
+ const nodeNameToCheckIfExists = isInstance ? owner.instanceNodeString(entry.item) : entry.key;
42754
+ const displayName = (entry.item && (entry.item.displayName || nodeNameToCheckIfExists)) || nodeNameToCheckIfExists;
42755
+ li.attr('data-display-name', displayName);
42756
+ let itemElem = li.select('.tsi-hierarchyItem');
42757
+ if (itemElem.empty()) {
42758
+ const newListElem = owner.createHierarchyItemElem(entry.item, entry.key);
42759
+ li.node().appendChild(newListElem.node());
42760
+ itemElem = li.select('.tsi-hierarchyItem');
42761
+ }
42762
+ itemElem.attr('aria-label', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
42763
+ itemElem.attr('title', isInstance ? owner.getAriaLabel(entry.item) : entry.key);
42764
+ itemElem.attr('aria-expanded', String(entry.item.isExpanded));
42765
+ // accessibility: set treeitem level and position in set
42766
+ const ariaLevel = String(((entry.item && typeof entry.item.level === 'number') ? entry.item.level : 0) + 1);
42767
+ itemElem.attr('aria-level', ariaLevel);
42768
+ itemElem.attr('aria-posinset', String(i + 1));
42769
+ itemElem.attr('aria-setsize', String(setSize));
42770
+ if (!isInstance) {
42771
+ itemElem.select('.tsi-caret-icon').attr('style', `left: ${(entry.item.level) * 18 + 20}px`);
42772
+ itemElem.select('.tsi-name').text(entry.key);
42773
+ itemElem.select('.tsi-instanceCount').text(entry.item.cumulativeInstanceCount);
42774
+ }
42775
+ else {
42776
+ const nameSpan = itemElem.select('.tsi-name');
42777
+ nameSpan.html('');
42778
+ Utils.appendFormattedElementsFromString(nameSpan, owner.instanceNodeStringToDisplay(entry.item));
42779
+ }
42780
+ entry.item.node = li;
42781
+ if (entry.item.children) {
42782
+ entry.item.isExpanded = true;
42783
+ li.classed('tsi-expanded', true);
42784
+ // recurse using TreeRenderer to keep rendering logic centralized
42785
+ TreeRenderer.render(owner, entry.item.children, li);
42786
+ }
42787
+ else {
42788
+ li.classed('tsi-expanded', false);
42789
+ li.selectAll('ul').remove();
42790
+ }
42791
+ });
42792
+ }
42793
+ }
42794
+
43788
42795
  class HierarchyNavigation extends Component {
43789
42796
  constructor(renderTarget) {
43790
42797
  super(renderTarget);
43791
42798
  this.path = [];
42799
+ // debounce + request cancellation fields
42800
+ this.debounceTimer = null;
42801
+ this.debounceDelay = 250; // ms
42802
+ this.requestCounter = 0; // increments for each outgoing request
42803
+ this.latestRequestId = 0; // id of the most recent request
43792
42804
  //selectedIds
43793
42805
  this.selectedIds = [];
43794
42806
  this.searchEnabled = true;
43795
- this.renderSearchResult = (r, payload, target) => {
42807
+ this.autocompleteEnabled = true; // Enable/disable autocomplete suggestions
42808
+ // Search mode state
42809
+ this.isSearchMode = false;
42810
+ // Paths that should be auto-expanded (Set of path strings like "Factory North/Building A")
42811
+ this.pathsToAutoExpand = new Set();
42812
+ this.renderSearchResult = async (r, payload, target) => {
43796
42813
  const hierarchyData = r.hierarchyNodes?.hits?.length
43797
42814
  ? this.fillDataRecursively(r.hierarchyNodes, payload, payload)
43798
42815
  : {};
43799
42816
  const instancesData = r.instances?.hits?.length
43800
42817
  ? r.instances.hits.reduce((acc, i) => {
43801
- acc[this.instanceNodeIdentifier(i)] = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
42818
+ const inst = new InstanceNode(i.timeSeriesId, i.name, payload.path.length - this.path.length, i.id, i.description);
42819
+ inst.displayName = this.instanceNodeStringToDisplay(i) || '';
42820
+ acc[this.instanceNodeIdentifier(i)] = inst;
43802
42821
  return acc;
43803
42822
  }, {})
43804
42823
  : {};
@@ -43809,7 +42828,17 @@
43809
42828
  }
43810
42829
  hitCountElem.text(r.hierarchyNodes.hitCount);
43811
42830
  }
43812
- this.renderTree({ ...hierarchyData, ...instancesData }, target);
42831
+ const merged = { ...hierarchyData, ...instancesData };
42832
+ this.renderTree(merged, target);
42833
+ // Auto-expand nodes that should be expanded and load their children
42834
+ for (const key in hierarchyData) {
42835
+ const node = hierarchyData[key];
42836
+ if (node.isExpanded && !node.children) {
42837
+ // This node should be expanded but doesn't have children loaded yet
42838
+ // We need to trigger expansion after the node is rendered
42839
+ await this.autoExpandNode(node);
42840
+ }
42841
+ }
43813
42842
  };
43814
42843
  this.hierarchyNodeIdentifier = (hName) => {
43815
42844
  return hName ? hName : '(' + this.getString("Empty") + ')';
@@ -43831,12 +42860,20 @@
43831
42860
  const targetElement = select(this.renderTarget).text('');
43832
42861
  this.hierarchyNavWrapper = this.createHierarchyNavWrapper(targetElement);
43833
42862
  this.selectedIds = preselectedIds;
42863
+ // Allow disabling autocomplete via options
42864
+ if (hierarchyNavOptions.autocompleteEnabled !== undefined) {
42865
+ this.autocompleteEnabled = hierarchyNavOptions.autocompleteEnabled;
42866
+ }
42867
+ // Pre-compute paths that need to be auto-expanded for preselected instances
42868
+ if (preselectedIds && preselectedIds.length > 0) {
42869
+ await this.computePathsToAutoExpand(preselectedIds);
42870
+ }
43834
42871
  //render search wrapper
43835
- //this.renderSearchBox()
42872
+ this.renderSearchBox();
43836
42873
  super.themify(this.hierarchyNavWrapper, this.chartOptions.theme);
43837
42874
  const results = this.createResultsWrapper(this.hierarchyNavWrapper);
43838
42875
  this.hierarchyElem = this.createHierarchyElem(results);
43839
- this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
42876
+ await this.pathSearchAndRenderResult({ search: { payload: this.requestPayload() }, render: { target: this.hierarchyElem } });
43840
42877
  }
43841
42878
  createHierarchyNavWrapper(targetElement) {
43842
42879
  return targetElement.append('div').attr('class', 'tsi-hierarchy-nav-wrapper');
@@ -43844,8 +42881,129 @@
43844
42881
  createResultsWrapper(hierarchyNavWrapper) {
43845
42882
  return hierarchyNavWrapper.append('div').classed('tsi-hierarchy-or-list-wrapper', true);
43846
42883
  }
42884
+ // create hierarchy container and attach keyboard handler
43847
42885
  createHierarchyElem(results) {
43848
- return results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
42886
+ const sel = results.append('div').classed('tsi-hierarchy', true).attr("role", "navigation").on('scroll', () => { });
42887
+ // attach keydown listener for keyboard navigation (delegated)
42888
+ // use native event to preserve focus handling
42889
+ const node = sel.node();
42890
+ if (node) {
42891
+ node.addEventListener('keydown', (ev) => this.onKeyDown(ev));
42892
+ }
42893
+ return sel;
42894
+ }
42895
+ // Keyboard navigation handlers and helpers
42896
+ onKeyDown(ev) {
42897
+ const key = ev.key;
42898
+ const active = document.activeElement;
42899
+ const container = this.hierarchyElem?.node();
42900
+ if (!container)
42901
+ return;
42902
+ const isInside = active && container.contains(active);
42903
+ if (!isInside && (key === 'ArrowDown' || key === 'ArrowUp')) {
42904
+ // focus first visible item on navigation keys
42905
+ const visible = this.getVisibleItemElems();
42906
+ if (visible.length) {
42907
+ this.focusItem(visible[0]);
42908
+ ev.preventDefault();
42909
+ }
42910
+ return;
42911
+ }
42912
+ if (!active)
42913
+ return;
42914
+ const current = active.classList && active.classList.contains('tsi-hierarchyItem') ? active : active.closest('.tsi-hierarchyItem');
42915
+ if (!current)
42916
+ return;
42917
+ switch (key) {
42918
+ case 'ArrowDown':
42919
+ this.focusNext(current);
42920
+ ev.preventDefault();
42921
+ break;
42922
+ case 'ArrowUp':
42923
+ this.focusPrev(current);
42924
+ ev.preventDefault();
42925
+ break;
42926
+ case 'ArrowRight':
42927
+ this.handleArrowRight(current);
42928
+ ev.preventDefault();
42929
+ break;
42930
+ case 'ArrowLeft':
42931
+ this.handleArrowLeft(current);
42932
+ ev.preventDefault();
42933
+ break;
42934
+ case 'Enter':
42935
+ case ' ':
42936
+ // activate (toggle expand or select)
42937
+ current.click();
42938
+ ev.preventDefault();
42939
+ break;
42940
+ }
42941
+ }
42942
+ getVisibleItemElems() {
42943
+ if (!this.hierarchyElem)
42944
+ return [];
42945
+ const root = this.hierarchyElem.node();
42946
+ if (!root)
42947
+ return [];
42948
+ const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
42949
+ return items.filter(i => i.offsetParent !== null && getComputedStyle(i).display !== 'none');
42950
+ }
42951
+ focusItem(elem) {
42952
+ if (!this.hierarchyElem)
42953
+ return;
42954
+ const root = this.hierarchyElem.node();
42955
+ if (!root)
42956
+ return;
42957
+ const items = Array.from(root.querySelectorAll('.tsi-hierarchyItem'));
42958
+ items.forEach(i => i.setAttribute('tabindex', '-1'));
42959
+ elem.setAttribute('tabindex', '0');
42960
+ elem.focus();
42961
+ }
42962
+ focusNext(current) {
42963
+ const visible = this.getVisibleItemElems();
42964
+ const idx = visible.indexOf(current);
42965
+ if (idx >= 0 && idx < visible.length - 1) {
42966
+ this.focusItem(visible[idx + 1]);
42967
+ }
42968
+ }
42969
+ focusPrev(current) {
42970
+ const visible = this.getVisibleItemElems();
42971
+ const idx = visible.indexOf(current);
42972
+ if (idx > 0) {
42973
+ this.focusItem(visible[idx - 1]);
42974
+ }
42975
+ }
42976
+ handleArrowRight(current) {
42977
+ const caret = current.querySelector('.tsi-caret-icon');
42978
+ const expanded = current.getAttribute('aria-expanded') === 'true';
42979
+ if (caret && !expanded) {
42980
+ // expand
42981
+ current.click();
42982
+ return;
42983
+ }
42984
+ // if already expanded, move to first child
42985
+ if (caret && expanded) {
42986
+ const li = current.closest('li');
42987
+ const childLi = li?.querySelector('ul > li');
42988
+ const childItem = childLi?.querySelector('.tsi-hierarchyItem');
42989
+ if (childItem)
42990
+ this.focusItem(childItem);
42991
+ }
42992
+ }
42993
+ handleArrowLeft(current) {
42994
+ const caret = current.querySelector('.tsi-caret-icon');
42995
+ const expanded = current.getAttribute('aria-expanded') === 'true';
42996
+ if (caret && expanded) {
42997
+ // collapse
42998
+ current.click();
42999
+ return;
43000
+ }
43001
+ // move focus to parent
43002
+ const li = current.closest('li');
43003
+ const parentLi = li?.parentElement?.closest('li');
43004
+ const parentItem = parentLi?.querySelector('.tsi-hierarchyItem');
43005
+ if (parentItem)
43006
+ this.focusItem(parentItem);
43849
43007
  }
43850
43008
  // prepares the parameters for search request
43851
43009
  requestPayload(hierarchy = null) {
@@ -43854,32 +43012,7 @@
43854
43012
  }
43855
43013
  // 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
43856
43014
  renderTree(data, target) {
43857
- let list = target.append('ul').attr("role", target === this.hierarchyElem ? "tree" : "group");
43858
- Object.keys(data).forEach(el => {
43859
- let nodeNameToCheckIfExists = data[el] instanceof InstanceNode ? this.instanceNodeString(data[el]) : el;
43860
- let li;
43861
- if (list.selectAll(".tsi-name").nodes().find(e => e.innerText === nodeNameToCheckIfExists)) {
43862
- li = null;
43863
- }
43864
- else {
43865
- li = list.append('li').classed('tsi-leaf', data[el].isLeaf);
43866
- //if the node is already selected, we want to highlight it
43867
- if (this.selectedIds && this.selectedIds.includes(data[el].id)) {
43868
- li.classed('tsi-selected', true);
43869
- }
43870
- }
43871
- if (!li)
43872
- return;
43873
- li.attr("role", "none");
43874
- let newListElem = this.createHierarchyItemElem(data[el], el);
43875
- li.node().appendChild(newListElem.node());
43876
- data[el].node = li;
43877
- if (data[el].children) {
43878
- data[el].isExpanded = true;
43879
- data[el].node.classed('tsi-expanded', true);
43880
- this.renderTree(data[el].children, data[el].node);
43881
- }
43882
- });
43015
+ TreeRenderer.render(this, data, target);
43883
43016
  }
43884
43017
  renderSearchBox() {
43885
43018
  this.searchWrapperElem = this.hierarchyNavWrapper.append('div').classed('tsi-hierarchy-search', true);
@@ -43888,40 +43021,140 @@
43888
43021
  let input = inputWrapper
43889
43022
  .append("input")
43890
43023
  .attr("class", "tsi-searchInput")
43891
- .attr("aria-label", this.getString("Search Time Series Instances"))
43892
- .attr("aria-describedby", "tsi-search-desc")
43024
+ .attr("aria-label", this.getString("Search"))
43025
+ .attr("aria-describedby", "tsi-hierarchy-search-desc")
43893
43026
  .attr("role", "combobox")
43894
43027
  .attr("aria-owns", "tsi-search-results")
43895
43028
  .attr("aria-expanded", "false")
43896
43029
  .attr("aria-haspopup", "listbox")
43897
- .attr("placeholder", this.getString("Search Time Series Instances") + "...");
43030
+ .attr("placeholder", this.getString("Search") + "...");
43031
+ // Add ARIA description for screen readers
43032
+ inputWrapper
43033
+ .append("span")
43034
+ .attr("id", "tsi-hierarchy-search-desc")
43035
+ .style("display", "none")
43036
+ .text(this.getString("Search suggestion instruction") || "Use arrow keys to navigate suggestions");
43037
+ // Add live region for search results info
43038
+ inputWrapper
43039
+ .append("div")
43040
+ .attr("class", "tsi-search-results-info")
43041
+ .attr("aria-live", "assertive");
43042
+ // Add clear button
43043
+ let clear = inputWrapper
43044
+ .append("div")
43045
+ .attr("class", "tsi-clear")
43046
+ .attr("tabindex", "0")
43047
+ .attr("role", "button")
43048
+ .attr("aria-label", "Clear Search")
43049
+ .on("click keydown", function (event) {
43050
+ if (Utils.isKeyDownAndNotEnter(event)) {
43051
+ return;
43052
+ }
43053
+ input.node().value = "";
43054
+ self.exitSearchMode();
43055
+ self.ap.close();
43056
+ select(this).classed("tsi-shown", false);
43057
+ });
43058
+ // Initialize Awesomplete for autocomplete (only if enabled)
43059
+ let Awesomplete = window.Awesomplete;
43060
+ if (this.autocompleteEnabled && Awesomplete) {
43061
+ this.ap = new Awesomplete(input.node(), {
43062
+ minChars: 1,
43063
+ maxItems: 10,
43064
+ autoFirst: true
43065
+ });
43066
+ }
43067
+ else {
43068
+ // Create a dummy object if autocomplete is disabled
43069
+ this.ap = {
43070
+ list: [],
43071
+ close: () => { },
43072
+ evaluate: () => { }
43073
+ };
43074
+ }
43898
43075
  let self = this;
43076
+ let noSuggest = false;
43077
+ let justAwesompleted = false;
43078
+ // Handle autocomplete selection (only if enabled)
43079
+ if (this.autocompleteEnabled) {
43080
+ input.node().addEventListener("awesomplete-selectcomplete", (event) => {
43081
+ noSuggest = true;
43082
+ const selectedValue = event.text.value;
43083
+ // Trigger search with selected value
43084
+ self.performDeepSearch(selectedValue);
43085
+ justAwesompleted = true;
43086
+ });
43087
+ }
43899
43088
  input.on("keydown", (event) => {
43089
+ // Handle ESC key to clear the search box
43090
+ if (event.key === 'Escape') {
43091
+ const inputElement = event.target;
43092
+ inputElement.value = '';
43093
+ // Trigger input event to clear search results
43094
+ self.exitSearchMode();
43095
+ self.ap.close();
43096
+ clear.classed("tsi-shown", false);
43097
+ return;
43098
+ }
43900
43099
  this.chartOptions.onKeydown(event, this.ap);
43901
43100
  });
43902
- var searchText;
43101
+ input.node().addEventListener("keyup", function (event) {
43102
+ if (justAwesompleted) {
43103
+ justAwesompleted = false;
43104
+ return;
43105
+ }
43106
+ let key = event.which || event.keyCode;
43107
+ if (key === 13) {
43108
+ noSuggest = true;
43109
+ }
43110
+ });
43111
+ // Debounced input handler to reduce work while typing
43903
43112
  input.on("input", function (event) {
43904
- searchText = event.target.value;
43905
- if (searchText.length === 0) {
43906
- //clear the tree
43907
- self.hierarchyElem.selectAll('ul').remove();
43908
- self.pathSearchAndRenderResult({ search: { payload: self.requestPayload() }, render: { target: self.hierarchyElem } });
43113
+ const val = event.target.value;
43114
+ // always clear existing timer
43115
+ if (self.debounceTimer) {
43116
+ clearTimeout(self.debounceTimer);
43117
+ self.debounceTimer = null;
43118
+ }
43119
+ // Show/hide clear button
43120
+ clear.classed("tsi-shown", val.length > 0);
43121
+ if (!val || val.length === 0) {
43122
+ // Exit search mode and restore navigation view
43123
+ self.exitSearchMode();
43124
+ self.ap.close();
43125
+ return;
43126
+ }
43127
+ // Populate autocomplete suggestions with instance leaves (only if enabled)
43128
+ if (self.autocompleteEnabled && !noSuggest && val.length >= 1) {
43129
+ self.fetchAutocompleteSuggestions(val);
43909
43130
  }
43910
43131
  else {
43911
- //filter the tree
43912
- self.filterTree(searchText);
43132
+ self.ap.close();
43913
43133
  }
43134
+ // Use deep search for comprehensive results
43135
+ self.debounceTimer = setTimeout(() => {
43136
+ self.performDeepSearch(val);
43137
+ }, self.debounceDelay);
43138
+ noSuggest = false;
43914
43139
  });
43915
43140
  }
43916
43141
  async pathSearchAndRenderResult({ search: { payload, bubbleUpReject = false }, render: { target, locInTarget = null } }) {
43142
+ const requestId = ++this.requestCounter;
43143
+ this.latestRequestId = requestId;
43917
43144
  try {
43918
43145
  const result = await this.searchFunction(payload);
43146
+ if (requestId !== this.latestRequestId) {
43147
+ return;
43148
+ }
43919
43149
  if (result.error) {
43920
43150
  throw result.error;
43921
43151
  }
43922
- this.renderSearchResult(result, payload, target);
43152
+ await this.renderSearchResult(result, payload, target);
43923
43153
  }
43924
43154
  catch (err) {
43155
+ if (requestId !== this.latestRequestId) {
43156
+ return;
43157
+ }
43925
43158
  this.chartOptions.onError("Error in hierarchy navigation", "Failed to complete search", err instanceof XMLHttpRequest ? err : null);
43926
43159
  if (bubbleUpReject) {
43927
43160
  throw err;
@@ -43929,11 +43162,18 @@
43929
43162
  }
43930
43163
  }
43931
43164
  filterTree(searchText) {
43932
- let tree = this.hierarchyElem.selectAll('ul').nodes()[0];
43933
- let list = tree.querySelectorAll('li');
43165
+ const nodes = this.hierarchyElem.selectAll('ul').nodes();
43166
+ if (!nodes || !nodes.length)
43167
+ return;
43168
+ const tree = nodes[0];
43169
+ if (!tree)
43170
+ return;
43171
+ const list = tree.querySelectorAll('li');
43172
+ const needle = searchText.toLowerCase();
43934
43173
  list.forEach((li) => {
43935
- let name = li.querySelector('.tsi-name').innerText;
43936
- if (name.toLowerCase().includes(searchText.toLowerCase())) {
43174
+ const attrName = li.getAttribute('data-display-name');
43175
+ let name = attrName && attrName.length ? attrName : (li.querySelector('.tsi-name')?.textContent || '');
43176
+ if (name.toLowerCase().includes(needle)) {
43937
43177
  li.style.display = 'block';
43938
43178
  }
43939
43179
  else {
@@ -43941,11 +43181,300 @@
43941
43181
  }
43942
43182
  });
43943
43183
  }
43184
+ // Fetch autocomplete suggestions for instances (leaves)
43185
+ async fetchAutocompleteSuggestions(searchText) {
43186
+ if (!searchText || searchText.length < 1) {
43187
+ this.ap.list = [];
43188
+ return;
43189
+ }
43190
+ try {
43191
+ // Call server search to get instance suggestions
43192
+ const payload = {
43193
+ path: this.path,
43194
+ searchTerm: searchText,
43195
+ recursive: true,
43196
+ includeInstances: true,
43197
+ // Limit results for autocomplete
43198
+ maxResults: 10
43199
+ };
43200
+ const results = await this.searchFunction(payload);
43201
+ if (results.error) {
43202
+ this.ap.list = [];
43203
+ return;
43204
+ }
43205
+ // Extract instance names for autocomplete suggestions
43206
+ const suggestions = [];
43207
+ if (results.instances?.hits) {
43208
+ results.instances.hits.forEach((i) => {
43209
+ const displayName = this.instanceNodeStringToDisplay(i);
43210
+ const pathStr = i.hierarchyPath && i.hierarchyPath.length > 0
43211
+ ? i.hierarchyPath.join(' > ') + ' > '
43212
+ : '';
43213
+ suggestions.push({
43214
+ label: pathStr + displayName,
43215
+ value: displayName
43216
+ });
43217
+ });
43218
+ }
43219
+ // Update Awesomplete list
43220
+ this.ap.list = suggestions;
43221
+ }
43222
+ catch (err) {
43223
+ // Silently fail for autocomplete - don't interrupt user experience
43224
+ this.ap.list = [];
43225
+ }
43226
+ }
43227
+ // Perform deep search across entire hierarchy using server-side search
43228
+ async performDeepSearch(searchText) {
43229
+ if (!searchText || searchText.length < 2) {
43230
+ this.exitSearchMode();
43231
+ return;
43232
+ }
43233
+ this.isSearchMode = true;
43234
+ const requestId = ++this.requestCounter;
43235
+ this.latestRequestId = requestId;
43236
+ try {
43237
+ // Call server search with recursive flag
43238
+ const payload = {
43239
+ path: this.path,
43240
+ searchTerm: searchText,
43241
+ recursive: true, // Search entire subtree
43242
+ includeInstances: true
43243
+ };
43244
+ const results = await this.searchFunction(payload);
43245
+ if (requestId !== this.latestRequestId)
43246
+ return; // Stale request
43247
+ if (results.error) {
43248
+ throw results.error;
43249
+ }
43250
+ // Render search results in flat list view
43251
+ this.renderSearchResults(results, searchText);
43252
+ }
43253
+ catch (err) {
43254
+ if (requestId !== this.latestRequestId)
43255
+ return;
43256
+ this.chartOptions.onError("Search failed", "Unable to search hierarchy", err instanceof XMLHttpRequest ? err : null);
43257
+ }
43258
+ }
43259
+ // Render search results with breadcrumb paths
43260
+ renderSearchResults(results, searchText) {
43261
+ this.hierarchyElem.selectAll('*').remove();
43262
+ const flatResults = [];
43263
+ // Flatten hierarchy results with full paths
43264
+ if (results.hierarchyNodes?.hits) {
43265
+ results.hierarchyNodes.hits.forEach((h) => {
43266
+ flatResults.push({
43267
+ type: 'hierarchy',
43268
+ name: h.name,
43269
+ path: h.path || [],
43270
+ id: h.id,
43271
+ cumulativeInstanceCount: h.cumulativeInstanceCount,
43272
+ highlightedName: this.highlightMatch(h.name, searchText),
43273
+ node: h
43274
+ });
43275
+ });
43276
+ }
43277
+ // Flatten instance results with full paths
43278
+ if (results.instances?.hits) {
43279
+ results.instances.hits.forEach((i) => {
43280
+ const displayName = this.instanceNodeStringToDisplay(i);
43281
+ flatResults.push({
43282
+ type: 'instance',
43283
+ name: i.name,
43284
+ path: i.hierarchyPath || [],
43285
+ id: i.id,
43286
+ timeSeriesId: i.timeSeriesId,
43287
+ description: i.description,
43288
+ highlightedName: this.highlightMatch(displayName, searchText),
43289
+ node: i
43290
+ });
43291
+ });
43292
+ }
43293
+ // Render flat list with breadcrumbs
43294
+ const searchList = this.hierarchyElem
43295
+ .append('div')
43296
+ .classed('tsi-search-results', true);
43297
+ if (flatResults.length === 0) {
43298
+ searchList.append('div')
43299
+ .classed('tsi-noResults', true)
43300
+ .text(this.getString('No results'));
43301
+ return;
43302
+ }
43303
+ searchList.append('div')
43304
+ .classed('tsi-search-results-header', true)
43305
+ .html(`<strong>${flatResults.length}</strong> ${this.getString('results found') || 'results found'}`);
43306
+ const resultItems = searchList.selectAll('.tsi-search-result-item')
43307
+ .data(flatResults)
43308
+ .enter()
43309
+ .append('div')
43310
+ .classed('tsi-search-result-item', true)
43311
+ .attr('tabindex', '0')
43312
+ .attr('role', 'option')
43313
+ .attr('aria-label', (d) => {
43314
+ const pathStr = d.path.length > 0 ? d.path.join(' > ') + ' > ' : '';
43315
+ return pathStr + d.name;
43316
+ });
43317
+ const self = this;
43318
+ resultItems.each(function (d) {
43319
+ const item = select(this);
43320
+ // Breadcrumb path
43321
+ if (d.path.length > 0) {
43322
+ item.append('div')
43323
+ .classed('tsi-search-breadcrumb', true)
43324
+ .text(d.path.join(' > '));
43325
+ }
43326
+ // Highlighted name
43327
+ item.append('div')
43328
+ .classed('tsi-search-result-name', true)
43329
+ .html(d.highlightedName);
43330
+ // Instance description or count
43331
+ if (d.type === 'instance' && d.description) {
43332
+ item.append('div')
43333
+ .classed('tsi-search-result-description', true)
43334
+ .text(d.description);
43335
+ }
43336
+ else if (d.type === 'hierarchy') {
43337
+ item.append('div')
43338
+ .classed('tsi-search-result-count', true)
43339
+ .text(`${d.cumulativeInstanceCount || 0} instances`);
43340
+ }
43341
+ });
43342
+ // Click handlers
43343
+ resultItems.on('click keydown', function (event, d) {
43344
+ if (Utils.isKeyDownAndNotEnter(event))
43345
+ return;
43346
+ if (d.type === 'instance') {
43347
+ // Handle instance selection
43348
+ if (self.chartOptions.onInstanceClick) {
43349
+ const inst = new InstanceNode(d.timeSeriesId, d.name, d.path.length, d.id, d.description);
43350
+ // Update selection state
43351
+ if (self.selectedIds && self.selectedIds.includes(d.id)) {
43352
+ self.selectedIds = self.selectedIds.filter(id => id !== d.id);
43353
+ select(this).classed('tsi-selected', false);
43354
+ }
43355
+ else {
43356
+ self.selectedIds.push(d.id);
43357
+ select(this).classed('tsi-selected', true);
43358
+ }
43359
+ self.chartOptions.onInstanceClick(inst);
43360
+ }
43361
+ }
43362
+ else {
43363
+ // Navigate to hierarchy node - exit search and expand to that path
43364
+ self.navigateToPath(d.path);
43365
+ }
43366
+ });
43367
+ // Apply selection state to already-selected instances
43368
+ resultItems.each(function (d) {
43369
+ if (d.type === 'instance' && self.selectedIds && self.selectedIds.includes(d.id)) {
43370
+ select(this).classed('tsi-selected', true);
43371
+ }
43372
+ });
43373
+ }
43374
+ // Exit search mode and restore tree
43375
+ exitSearchMode() {
43376
+ this.isSearchMode = false;
43377
+ this.hierarchyElem.selectAll('*').remove();
43378
+ this.pathSearchAndRenderResult({
43379
+ search: { payload: this.requestPayload() },
43380
+ render: { target: this.hierarchyElem }
43381
+ });
43382
+ }
43383
+ // Navigate to a specific path in the hierarchy
43384
+ async navigateToPath(targetPath) {
43385
+ this.exitSearchMode();
43386
+ // For now, just exit search mode and return to root
43387
+ // In a more advanced implementation, this would progressively
43388
+ // expand nodes along the path to reveal the target
43389
+ // This would require waiting for each level to load before expanding the next
43390
+ }
43391
+ // Pre-compute which paths need to be auto-expanded for preselected instances
43392
+ async computePathsToAutoExpand(instanceIds) {
43393
+ if (!instanceIds || instanceIds.length === 0) {
43394
+ return;
43395
+ }
43396
+ // console.log('[HierarchyNavigation] Computing paths to auto-expand for:', instanceIds);
43397
+ try {
43398
+ this.pathsToAutoExpand.clear();
43399
+ for (const instanceId of instanceIds) {
43400
+ // Search for this specific instance
43401
+ const result = await this.searchFunction({
43402
+ path: this.path,
43403
+ searchTerm: instanceId,
43404
+ recursive: true,
43405
+ includeInstances: true
43406
+ });
43407
+ if (result?.instances?.hits) {
43408
+ for (const instance of result.instances.hits) {
43409
+ // Match by ID
43410
+ if (instance.id === instanceId ||
43411
+ (instance.id && instance.id.includes(instanceId))) {
43412
+ if (instance.hierarchyPath && instance.hierarchyPath.length > 0) {
43413
+ // Add all parent paths that need to be expanded
43414
+ const hierarchyPath = instance.hierarchyPath;
43415
+ for (let i = 1; i <= hierarchyPath.length; i++) {
43416
+ const pathArray = hierarchyPath.slice(0, i);
43417
+ const pathKey = pathArray.join('/');
43418
+ this.pathsToAutoExpand.add(pathKey);
43419
+ }
43420
+ }
43421
+ }
43422
+ }
43423
+ }
43424
+ }
43425
+ // console.log('[HierarchyNavigation] Paths to auto-expand:', Array.from(this.pathsToAutoExpand));
43426
+ }
43427
+ catch (err) {
43428
+ console.warn('Failed to compute paths to auto-expand:', err);
43429
+ }
43430
+ }
43431
+ // Check if a path should be auto-expanded
43432
+ shouldAutoExpand(pathArray) {
43433
+ if (this.pathsToAutoExpand.size === 0) {
43434
+ return false;
43435
+ }
43436
+ const pathKey = pathArray.join('/');
43437
+ return this.pathsToAutoExpand.has(pathKey);
43438
+ }
43439
+ // Auto-expand a node by triggering its expand function
43440
+ async autoExpandNode(node) {
43441
+ if (!node || !node.expand || !node.node) {
43442
+ return;
43443
+ }
43444
+ try {
43445
+ // Wait for the DOM node to be available
43446
+ await new Promise(resolve => setTimeout(resolve, 10));
43447
+ // Mark as expanded visually
43448
+ node.node.classed('tsi-expanded', true);
43449
+ // Call the expand function to load children
43450
+ await node.expand();
43451
+ // console.log(`[HierarchyNavigation] Auto-expanded node: ${node.path.join('/')}`);
43452
+ }
43453
+ catch (err) {
43454
+ console.warn(`Failed to auto-expand node ${node.path.join('/')}:`, err);
43455
+ }
43456
+ }
43457
+ // Highlight search term in text
43458
+ highlightMatch(text, searchTerm) {
43459
+ if (!text)
43460
+ return '';
43461
+ const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
43462
+ const regex = new RegExp(`(${escapedTerm})`, 'gi');
43463
+ return text.replace(regex, '<mark>$1</mark>');
43464
+ }
43944
43465
  // creates in-depth data object using the server response for hierarchyNodes to show in the tree all expanded, considering UntilChildren
43945
43466
  fillDataRecursively(hierarchyNodes, payload, payloadForContinuation = null) {
43946
43467
  let data = {};
43947
43468
  hierarchyNodes.hits.forEach((h) => {
43948
43469
  let hierarchy = new HierarchyNode(h.name, payload.path, payload.path.length - this.path.length, h.cumulativeInstanceCount, h.id);
43470
+ // cache display name on node for client-side filtering
43471
+ hierarchy.displayName = h.name || '';
43472
+ // Check if this path should be auto-expanded
43473
+ const shouldExpand = this.shouldAutoExpand(hierarchy.path);
43474
+ if (shouldExpand) {
43475
+ hierarchy.isExpanded = true;
43476
+ //console.log(`[HierarchyNavigation] Auto-expanding node: ${hierarchy.path.join('/')}`);
43477
+ }
43949
43478
  hierarchy.expand = () => {
43950
43479
  hierarchy.isExpanded = true;
43951
43480
  hierarchy.node.classed('tsi-expanded', true);
@@ -43969,7 +43498,7 @@
43969
43498
  .attr('style', `padding-left: ${hORi.isLeaf ? hORi.level * 18 + 20 : (hORi.level + 1) * 18 + 20}px`)
43970
43499
  .attr('tabindex', 0)
43971
43500
  //.attr('arialabel', isHierarchyNode ? key : Utils.getTimeSeriesIdString(hORi))
43972
- .attr('arialabel', isHierarchyNode ? key : self.getAriaLabel(hORi))
43501
+ .attr('aria-label', isHierarchyNode ? key : self.getAriaLabel(hORi))
43973
43502
  .attr('title', isHierarchyNode ? key : self.getAriaLabel(hORi))
43974
43503
  .attr("role", "treeitem").attr('aria-expanded', hORi.isExpanded)
43975
43504
  .on('click keydown', async function (event) {
@@ -44021,6 +43550,8 @@
44021
43550
  return hORi.description || hORi.name || hORi.id || Utils.getTimeSeriesIdString(hORi);
44022
43551
  }
44023
43552
  }
43553
+ // TreeRenderer has been moved to its own module: ./TreeRenderer
43554
+ // The rendering logic was extracted to reduce file size and improve testability.
44024
43555
  class HierarchyNode {
44025
43556
  constructor(name, parentPath, level, cumulativeInstanceCount = null, id = null) {
44026
43557
  this.name = name;
@@ -44265,6 +43796,7 @@
44265
43796
  class DateTimeButtonSingle extends DateTimeButton {
44266
43797
  constructor(renderTarget) {
44267
43798
  super(renderTarget);
43799
+ this.clickOutsideHandler = null;
44268
43800
  this.sDTPOnSet = (millis = null) => {
44269
43801
  if (millis !== null) {
44270
43802
  this.dateTimeButton.text(this.buttonDateTimeFormat(millis));
@@ -44277,6 +43809,32 @@
44277
43809
  closeSDTP() {
44278
43810
  this.dateTimePickerContainer.style("display", "none");
44279
43811
  this.dateTimeButton.node().focus();
43812
+ this.removeClickOutsideHandler();
43813
+ }
43814
+ removeClickOutsideHandler() {
43815
+ if (this.clickOutsideHandler) {
43816
+ document.removeEventListener('click', this.clickOutsideHandler);
43817
+ this.clickOutsideHandler = null;
43818
+ }
43819
+ }
43820
+ setupClickOutsideHandler() {
43821
+ // Remove any existing handler first
43822
+ this.removeClickOutsideHandler();
43823
+ // Add handler after a small delay to prevent the opening click from immediately closing the picker
43824
+ setTimeout(() => {
43825
+ this.clickOutsideHandler = (event) => {
43826
+ const pickerElement = this.dateTimePickerContainer.node();
43827
+ const buttonElement = this.dateTimeButton.node();
43828
+ const target = event.target;
43829
+ // Check if click is outside both the picker and the button
43830
+ if (pickerElement && buttonElement &&
43831
+ !pickerElement.contains(target) &&
43832
+ !buttonElement.contains(target)) {
43833
+ this.closeSDTP();
43834
+ }
43835
+ };
43836
+ document.addEventListener('click', this.clickOutsideHandler);
43837
+ }, 0);
44280
43838
  }
44281
43839
  render(chartOptions = {}, minMillis, maxMillis, selectedMillis = null, onSet = null) {
44282
43840
  super.render(chartOptions, minMillis, maxMillis, onSet);
@@ -44286,12 +43844,11 @@
44286
43844
  if (!this.dateTimePicker) {
44287
43845
  this.dateTimePicker = new SingleDateTimePicker(this.dateTimePickerContainer.node());
44288
43846
  }
44289
- let targetElement = select(this.renderTarget);
44290
- (targetElement.select(".tsi-dateTimePickerContainer")).selectAll("*");
44291
43847
  this.dateTimeButton.on("click", () => {
44292
43848
  this.chartOptions.dTPIsModal = true;
44293
43849
  this.dateTimePickerContainer.style("display", "block");
44294
43850
  this.dateTimePicker.render(this.chartOptions, this.minMillis, this.maxMillis, this.selectedMillis, this.sDTPOnSet);
43851
+ this.setupClickOutsideHandler();
44295
43852
  });
44296
43853
  }
44297
43854
  }
@@ -44589,8 +44146,16 @@
44589
44146
  class PlaybackControls extends Component {
44590
44147
  constructor(renderTarget, initialTimeStamp = null) {
44591
44148
  super(renderTarget);
44592
- this.handleRadius = 7;
44593
- this.minimumPlaybackInterval = 1000; // 1 second
44149
+ this.playbackInterval = null;
44150
+ this.playButton = null;
44151
+ this.handleElement = null;
44152
+ this.controlsContainer = null;
44153
+ this.track = null;
44154
+ this.selectTimeStampCallback = null;
44155
+ this.wasPlayingWhenDragStarted = false;
44156
+ this.rafId = null;
44157
+ this.handleRadius = PlaybackControls.CONSTANTS.HANDLE_RADIUS;
44158
+ this.minimumPlaybackInterval = PlaybackControls.CONSTANTS.MINIMUM_PLAYBACK_INTERVAL_MS;
44594
44159
  this.playbackInterval = null;
44595
44160
  this.selectedTimeStamp = initialTimeStamp;
44596
44161
  }
@@ -44598,6 +44163,21 @@
44598
44163
  return this.selectedTimeStamp;
44599
44164
  }
44600
44165
  render(start, end, onSelectTimeStamp, options, playbackSettings) {
44166
+ // Validate inputs
44167
+ if (!(start instanceof Date) || !(end instanceof Date)) {
44168
+ throw new TypeError('start and end must be Date objects');
44169
+ }
44170
+ if (start >= end) {
44171
+ throw new RangeError('start must be before end');
44172
+ }
44173
+ if (!onSelectTimeStamp || typeof onSelectTimeStamp !== 'function') {
44174
+ throw new TypeError('onSelectTimeStamp must be a function');
44175
+ }
44176
+ // Clean up any pending animation frames before re-rendering
44177
+ if (this.rafId !== null) {
44178
+ cancelAnimationFrame(this.rafId);
44179
+ this.rafId = null;
44180
+ }
44601
44181
  this.end = end;
44602
44182
  this.selectTimeStampCallback = onSelectTimeStamp;
44603
44183
  this.chartOptions.setOptions(options);
@@ -44659,6 +44239,9 @@
44659
44239
  this.playButton = this.controlsContainer.append('button')
44660
44240
  .classed('tsi-play-button', this.playbackInterval === null)
44661
44241
  .classed('tsi-pause-button', this.playbackInterval !== null)
44242
+ // Accessibility attributes
44243
+ .attr('aria-label', 'Play/Pause playback')
44244
+ .attr('title', 'Play/Pause playback')
44662
44245
  .on('click', () => {
44663
44246
  if (this.playbackInterval === null) {
44664
44247
  this.play();
@@ -44710,6 +44293,27 @@
44710
44293
  this.updateSelection(handlePosition, this.selectedTimeStamp);
44711
44294
  this.selectTimeStampCallback(this.selectedTimeStamp);
44712
44295
  }
44296
+ /**
44297
+ * Cleanup resources to prevent memory leaks
44298
+ */
44299
+ destroy() {
44300
+ this.pause();
44301
+ // Cancel any pending animation frames
44302
+ if (this.rafId !== null) {
44303
+ cancelAnimationFrame(this.rafId);
44304
+ this.rafId = null;
44305
+ }
44306
+ // Remove event listeners
44307
+ if (this.controlsContainer) {
44308
+ this.controlsContainer.selectAll('*').on('.', null);
44309
+ }
44310
+ // Clear DOM references
44311
+ this.playButton = null;
44312
+ this.handleElement = null;
44313
+ this.controlsContainer = null;
44314
+ this.track = null;
44315
+ this.selectTimeStampCallback = null;
44316
+ }
44713
44317
  clamp(number, min, max) {
44714
44318
  let clamped = Math.max(number, min);
44715
44319
  return Math.min(clamped, max);
@@ -44718,9 +44322,17 @@
44718
44322
  this.wasPlayingWhenDragStarted = this.wasPlayingWhenDragStarted ||
44719
44323
  (this.playbackInterval !== null);
44720
44324
  this.pause();
44721
- let handlePosition = this.clamp(positionX, 0, this.trackWidth);
44722
- this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
44723
- this.updateSelection(handlePosition, this.selectedTimeStamp);
44325
+ // Use requestAnimationFrame to batch DOM updates for better performance
44326
+ // Cancel any pending animation frame to prevent stacking updates
44327
+ if (this.rafId !== null) {
44328
+ cancelAnimationFrame(this.rafId);
44329
+ }
44330
+ this.rafId = requestAnimationFrame(() => {
44331
+ const handlePosition = this.clamp(positionX, 0, this.trackWidth);
44332
+ this.selectedTimeStamp = this.timeStampToPosition.invert(handlePosition);
44333
+ this.updateSelection(handlePosition, this.selectedTimeStamp);
44334
+ this.rafId = null;
44335
+ });
44724
44336
  }
44725
44337
  onDragEnd() {
44726
44338
  this.selectTimeStampCallback(this.selectedTimeStamp);
@@ -44743,6 +44355,12 @@
44743
44355
  .text(this.timeFormatter(timeStamp));
44744
44356
  }
44745
44357
  }
44358
+ PlaybackControls.CONSTANTS = {
44359
+ HANDLE_RADIUS: 7,
44360
+ MINIMUM_PLAYBACK_INTERVAL_MS: 1000,
44361
+ HANDLE_PADDING: 8,
44362
+ AXIS_OFFSET: 6,
44363
+ };
44746
44364
  class TimeAxis extends TemporalXAxisComponent {
44747
44365
  constructor(renderTarget) {
44748
44366
  super(renderTarget);
@@ -45009,6 +44627,10 @@
45009
44627
  }
45010
44628
  }
45011
44629
 
44630
+ // Ensure moment is available globally for Pikaday and other components
44631
+ if (typeof window !== 'undefined') {
44632
+ window.moment = moment;
44633
+ }
45012
44634
  class UXClient {
45013
44635
  constructor() {
45014
44636
  // Public facing components have class constructors exposed as public UXClient members.