hcs-cli 0.1.320__py3-none-any.whl → 0.1.322__py3-none-any.whl

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.
@@ -6,18 +6,24 @@
6
6
  <style>
7
7
  body {
8
8
  font-family: sans-serif;
9
- padding: 20px;
9
+ padding: 0;
10
+ margin: 0;
10
11
  }
11
12
  .day-row {
12
13
  margin-bottom: 40px;
13
14
  position: relative;
15
+ padding: 0 40px;
16
+ display: flex;
17
+ align-items: flex-start;
18
+ gap: 20px;
14
19
  }
15
20
  .label {
16
21
  font-weight: bold;
17
22
  margin-bottom: 8px;
18
23
  display: flex;
19
- align-items: center;
20
- gap: 8px;
24
+ flex-direction: column;
25
+ align-items: flex-start;
26
+ gap: 4px;
21
27
  }
22
28
  .label-left {
23
29
  display: flex;
@@ -39,10 +45,10 @@
39
45
  align-items: flex-end;
40
46
  height: 200px;
41
47
  border-left: 1px solid #ccc;
42
- position: relative;
43
48
  border-bottom: 1px solid #ccc;
44
- width: 768px;
45
- margin-left: 20px;
49
+ flex: 1;
50
+ margin-left: 0;
51
+ min-width: 400px;
46
52
  }
47
53
  .bar-wrapper {
48
54
  position: relative;
@@ -89,9 +95,11 @@
89
95
  .y-axis-label {
90
96
  position: absolute;
91
97
  top: 30px;
92
- left: -10px;
98
+ left: -50px;
93
99
  font-size: 10px;
94
100
  font-weight: normal;
101
+ text-align: right;
102
+ width: 40px;
95
103
  }
96
104
  .x-tick {
97
105
  position: absolute;
@@ -176,16 +184,19 @@
176
184
  z-index: 10;
177
185
  }
178
186
  .container {
179
- width: 834px;
180
- margin: 0 auto;
181
- padding: 0 20px;
187
+ width: 100%;
188
+ max-width: none;
189
+ margin: 0;
190
+ padding: 0;
182
191
  }
183
192
  .header {
184
193
  display: flex;
185
194
  justify-content: space-between;
186
195
  align-items: center;
187
196
  margin-bottom: 20px;
188
- padding: 0 20px;
197
+ padding: 20px 40px;
198
+ background-color: #f9f9f9;
199
+ border-bottom: 1px solid #e0e0e0;
189
200
  }
190
201
  .timezone-selector {
191
202
  display: flex;
@@ -204,6 +215,7 @@
204
215
  /* Tab styles */
205
216
  .tab-container {
206
217
  margin-top: 20px;
218
+ padding: 0 40px;
207
219
  }
208
220
  .tab-buttons {
209
221
  display: flex;
@@ -265,9 +277,12 @@
265
277
  background-color: #e0e0e0;
266
278
  }
267
279
  #config-json {
268
- padding-top: 40px;
280
+ padding: 20px;
281
+ max-height: 600px;
282
+ overflow-y: auto;
269
283
  }
270
284
  </style>
285
+ <script src="https://echarts.apache.org/en/js/vendors/echarts/dist/echarts.min.js"></script>
271
286
  </head>
272
287
  <body>
273
288
 
@@ -278,37 +293,45 @@
278
293
  <span>Timezone:</span>
279
294
  <select id="timezone-select">
280
295
  <option value="UTC">UTC</option>
281
- <option value="America/New_York">America/New_York</option>
282
- <option value="America/Los_Angeles">America/Los_Angeles</option>
283
- <option value="Europe/London">Europe/London</option>
284
- <option value="Europe/Paris">Europe/Paris</option>
285
- <option value="Asia/Tokyo">Asia/Tokyo</option>
286
- <option value="Australia/Sydney">Australia/Sydney</option>
296
+ <option id="browser-timezone-option" value="Browser">Browser</option>
287
297
  </select>
288
298
  </div>
289
299
  </div>
290
-
300
+
291
301
  <div class="tab-container">
292
302
  <div class="tab-buttons">
293
- <button class="tab-button active" data-tab="capacity">Capacity</button>
303
+ <button class="tab-button active" data-tab="usage">Usage</button>
304
+ <button class="tab-button" data-tab="capacity">Capacity</button>
294
305
  <button class="tab-button" data-tab="configuration">Configuration</button>
295
- <button class="tab-button" data-tab="history">History</button>
306
+ <button class="tab-button" data-tab="data">Data</button>
307
+ </div>
308
+
309
+ <div class="tab-content active" id="usage-tab">
310
+ <div style="margin-bottom: 10px; display: flex; justify-content: flex-end; align-items: center; gap: 10px;">
311
+ <button id="reset-chart-zoom" style="padding: 6px 12px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; display: none;">Reset Time Range</button>
312
+ <span id="selection-info" style="font-size: 12px; color: #666; font-weight: 500;">All timepoints</span>
313
+ </div>
314
+ <div style="width: 100%; height: 400px; position: relative;">
315
+ <div id="usage-chart" style="width: 100%; height: 100%; cursor: crosshair;"></div>
316
+ <div id="chart-selection-overlay" style="position: absolute; top: 0; left: 0; width: 0; height: 100%; z-index: 10; display: none; background: rgba(100, 150, 255, 0.2); border-left: 2px solid #0066ff; border-right: 2px solid #0066ff; pointer-events: none;"></div>
317
+ </div>
296
318
  </div>
297
-
298
- <div class="tab-content active" id="capacity-tab">
319
+
320
+ <div class="tab-content" id="capacity-tab">
299
321
  <div class="week-stats"></div>
300
322
  <div class="chart-container">
301
323
  <div id="chart"></div>
302
324
  </div>
303
325
  </div>
304
-
326
+
305
327
  <div class="tab-content" id="configuration-tab">
306
328
  <button class="copy-button" id="copy-config">Copy to Clipboard</button>
307
329
  <div class="code-container" id="config-json"></div>
308
330
  </div>
309
331
 
310
- <div class="tab-content" id="history-tab">
311
- <!-- History content will go here -->
332
+ <div class="tab-content" id="data-tab">
333
+ <button class="copy-button" id="copy-data">Copy to Clipboard</button>
334
+ <div class="code-container" id="chart-data-json"></div>
312
335
  </div>
313
336
  </div>
314
337
  </div>
@@ -340,7 +363,7 @@
340
363
 
341
364
  function updateBarColor(bar, idealCapacity, calculatedCapacity) {
342
365
  const effectiveCalculatedCapacity = calculatedCapacity === undefined ? 0 : calculatedCapacity;
343
-
366
+
344
367
  if (idealCapacity < effectiveCalculatedCapacity) {
345
368
  bar.style.backgroundColor = '#ffa3a3'; // light red
346
369
  } else if (idealCapacity > effectiveCalculatedCapacity * 1.1) {
@@ -363,7 +386,7 @@
363
386
  const forecastCapacity = slotRecord.forecastCapacity || 0;
364
387
  const calculatedCapacity = slotRecord.calculatedCapacity || 0;
365
388
  const idealCapacity = slotRecord.idealCapacity || 0;
366
-
389
+
367
390
  // Calculate delta between calculated and forecast
368
391
  const delta = calculatedCapacity - forecastCapacity;
369
392
  if (delta > 0) {
@@ -410,14 +433,16 @@
410
433
 
411
434
  const label = document.createElement('div');
412
435
  label.className = 'label';
413
-
436
+
414
437
  const labelLeft = document.createElement('div');
415
438
  labelLeft.className = 'label-left';
416
-
439
+
417
440
  const dayName = document.createElement('span');
418
441
  dayName.textContent = weekday.charAt(0).toUpperCase() + weekday.slice(1);
419
442
  labelLeft.appendChild(dayName);
420
443
 
444
+ label.appendChild(labelLeft);
445
+
421
446
  const applyCalculated = document.createElement('span');
422
447
  applyCalculated.className = 'apply-calculated';
423
448
  applyCalculated.textContent = 'apply calculated';
@@ -426,15 +451,15 @@
426
451
  const slotKey = `${weekday}/${time}`;
427
452
  const slotRecord = calendarData.calendar[slotKey] || {};
428
453
  const calculatedCapacity = slotRecord.calculatedCapacity;
429
-
454
+
430
455
  // Initialize slot if it doesn't exist
431
456
  if (!calendarData.calendar[slotKey]) {
432
457
  calendarData.calendar[slotKey] = {};
433
458
  }
434
-
459
+
435
460
  // Set idealCapacity to calculatedCapacity (0 if undefined)
436
461
  calendarData.calendar[slotKey].idealCapacity = calculatedCapacity || 0;
437
-
462
+
438
463
  // Update the bar height and color
439
464
  const bar = document.querySelector(`[data-key="${slotKey}"] .bar`);
440
465
  const barWrapper = document.querySelector(`[data-key="${slotKey}"]`);
@@ -457,9 +482,7 @@
457
482
  });
458
483
  updateWeekStats();
459
484
  };
460
- labelLeft.appendChild(applyCalculated);
461
-
462
- label.appendChild(labelLeft);
485
+ label.appendChild(applyCalculated);
463
486
 
464
487
  // Add day stats
465
488
  const dayStats = document.createElement('span');
@@ -475,7 +498,7 @@
475
498
  const slotRecord = calendarData.calendar[slotKey] || {};
476
499
  const forecastCapacity = slotRecord.forecastCapacity || 0;
477
500
  const calculatedCapacity = slotRecord.calculatedCapacity || 0;
478
-
501
+
479
502
  const delta = calculatedCapacity - forecastCapacity;
480
503
  if (delta > 0) {
481
504
  neededVmHours += delta;
@@ -519,7 +542,7 @@
519
542
  const idealCapacity = slotRecord.idealCapacity || 0;
520
543
  const forecastCapacity = slotRecord.forecastCapacity;
521
544
  const calculatedCapacity = slotRecord.calculatedCapacity === undefined ? 0 : slotRecord.calculatedCapacity;
522
-
545
+
523
546
  // Create gauge container
524
547
  const barWrapper = document.createElement('div');
525
548
  barWrapper.className = 'bar-wrapper';
@@ -549,7 +572,7 @@
549
572
  const clickY = e.clientY - rect.top;
550
573
  const ratio = 1 - (clickY / rect.height);
551
574
  const newValue = Math.round(ratio * maxCapacity);
552
-
575
+
553
576
  if (!calendarData.calendar[key]) {
554
577
  calendarData.calendar[key] = {};
555
578
  }
@@ -583,7 +606,7 @@
583
606
  const clickY = e.clientY - rect.top;
584
607
  const ratio = 1 - (clickY / rect.height);
585
608
  const newValue = Math.round(ratio * maxCapacity);
586
-
609
+
587
610
  if (!calendarData.calendar[key]) {
588
611
  calendarData.calendar[key] = {};
589
612
  }
@@ -687,10 +710,6 @@
687
710
  tooltip.style.display = 'block';
688
711
  };
689
712
 
690
- barWrapper.onmousemove = function(e) {
691
- updateTooltipContent(e, key);
692
- };
693
-
694
713
  barWrapper.onmouseleave = function() {
695
714
  barWrapper.style.backgroundColor = '#f0f0f0';
696
715
  tooltip.style.display = 'none';
@@ -759,35 +778,520 @@
759
778
  formatJsonForDisplay(calendarData);
760
779
  }
761
780
 
781
+ function displayChartData() {
782
+ const usageChartData = {{USAGE_CHART_DATA}};
783
+ const container = document.getElementById('chart-data-json');
784
+ if (usageChartData) {
785
+ container.textContent = JSON.stringify(usageChartData, null, 2);
786
+ } else {
787
+ container.textContent = 'No usage data available';
788
+ }
789
+ }
790
+
791
+ function convertTimeToTimezone(timeStr, timezone) {
792
+ // timeStr is in format "YYYY-MM-DDTHH:MM"
793
+ // Convert to the specified timezone
794
+ try {
795
+ const date = new Date(timeStr + 'Z'); // Parse as UTC
796
+ let tzToUse = timezone;
797
+
798
+ // If Browser timezone is selected, get the browser's timezone
799
+ if (timezone === 'Browser') {
800
+ tzToUse = Intl.DateTimeFormat().resolvedOptions().timeZone;
801
+ }
802
+
803
+ const formatter = new Intl.DateTimeFormat('en-US', {
804
+ year: 'numeric',
805
+ month: '2-digit',
806
+ day: '2-digit',
807
+ hour: '2-digit',
808
+ minute: '2-digit',
809
+ timeZone: tzToUse,
810
+ hour12: false
811
+ });
812
+
813
+ const parts = formatter.formatToParts(date);
814
+ const values = {};
815
+ parts.forEach(part => {
816
+ if (part.type !== 'literal') {
817
+ values[part.type] = part.value;
818
+ }
819
+ });
820
+
821
+ return `${values.year}-${values.month}-${values.day} ${values.hour}:${values.minute}`;
822
+ } catch (e) {
823
+ return timeStr;
824
+ }
825
+ }
826
+
827
+ let globalUsageChart = null; // Store chart instance for drag selection
828
+ let isSelectingTimeRange = false; // Global flag for selection mode
829
+ let currentFilteredStartIdx = null; // Track current time range filter start
830
+ let currentFilteredEndIdx = null; // Track current time range filter end
831
+
832
+ function renderUsageChart(filteredStartIdx = null, filteredEndIdx = null, timezone = 'UTC') {
833
+ // Store the current filter indices
834
+ currentFilteredStartIdx = filteredStartIdx;
835
+ currentFilteredEndIdx = filteredEndIdx;
836
+
837
+ const usageChartData = {{USAGE_CHART_DATA}};
838
+ if (!usageChartData) {
839
+ return;
840
+ }
841
+
842
+ let xAxis = usageChartData.x_axis.map(time => convertTimeToTimezone(time, timezone));
843
+ let chartData = {
844
+ consumed_capacity: usageChartData.consumed_capacity,
845
+ spare_capacity: usageChartData.spare_capacity,
846
+ no_spare_error: usageChartData.no_spare_error,
847
+ powered_on_vms: usageChartData.powered_on_vms,
848
+ consumed_capacity_predicated: usageChartData.consumed_capacity_predicated,
849
+ spare_capacity_predicated: usageChartData.spare_capacity_predicated,
850
+ no_spare_error_predicated: usageChartData.no_spare_error_predicated,
851
+ optimized_capacity: usageChartData.optimized_capacity,
852
+ };
853
+
854
+ // Apply time range filter if specified
855
+ if (filteredStartIdx !== null && filteredEndIdx !== null) {
856
+ const startIdx = Math.min(filteredStartIdx, filteredEndIdx);
857
+ const endIdx = Math.max(filteredStartIdx, filteredEndIdx);
858
+
859
+ xAxis = xAxis.slice(startIdx, endIdx + 1);
860
+ chartData.consumed_capacity = chartData.consumed_capacity.slice(startIdx, endIdx + 1);
861
+ chartData.spare_capacity = chartData.spare_capacity.slice(startIdx, endIdx + 1);
862
+ chartData.no_spare_error = chartData.no_spare_error.slice(startIdx, endIdx + 1);
863
+ chartData.powered_on_vms = chartData.powered_on_vms.slice(startIdx, endIdx + 1);
864
+ chartData.consumed_capacity_predicated = chartData.consumed_capacity_predicated.slice(startIdx, endIdx + 1);
865
+ chartData.spare_capacity_predicated = chartData.spare_capacity_predicated.slice(startIdx, endIdx + 1);
866
+ chartData.no_spare_error_predicated = chartData.no_spare_error_predicated.slice(startIdx, endIdx + 1);
867
+ chartData.optimized_capacity = chartData.optimized_capacity.slice(startIdx, endIdx + 1);
868
+
869
+ // Show reset button when zoomed and display timezone-aware times
870
+ document.getElementById('reset-chart-zoom').style.display = 'inline-block';
871
+ const startTime = xAxis[0];
872
+ const endTime = xAxis[xAxis.length - 1];
873
+ const timezone = document.getElementById('timezone-select').value;
874
+ const displayStart = convertTimeToTimezone(startTime, timezone);
875
+ const displayEnd = convertTimeToTimezone(endTime, timezone);
876
+ document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
877
+ } else {
878
+ // Hide reset button when not zoomed (showing default all timepoints)
879
+ document.getElementById('reset-chart-zoom').style.display = 'none';
880
+ // Display full time range with start and end times
881
+ const startTime = xAxis[0];
882
+ const endTime = xAxis[xAxis.length - 1];
883
+ const timezone = document.getElementById('timezone-select').value;
884
+ const displayStart = convertTimeToTimezone(startTime, timezone);
885
+ const displayEnd = convertTimeToTimezone(endTime, timezone);
886
+ document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
887
+ }
888
+
889
+ const chartDom = document.getElementById('usage-chart');
890
+ globalUsageChart = echarts.init(chartDom);
891
+ const myChart = globalUsageChart;
892
+ const option = {
893
+ animation: false,
894
+ title: {
895
+ text: "Template Usage"
896
+ },
897
+ tooltip: {
898
+ trigger: 'axis',
899
+ axisPointer: {
900
+ type: 'cross',
901
+ label: {
902
+ backgroundColor: '#6a7985',
903
+ formatter: function(params) {
904
+ // Only round Y-axis values, keep X-axis unchanged
905
+ if (params.axisDimension === 'y') {
906
+ return String(Math.round(params.value));
907
+ }
908
+ return params.value;
909
+ }
910
+ }
911
+ },
912
+ formatter: function(params) {
913
+ // Suppress tooltip when in time-range selection mode
914
+ if (isSelectingTimeRange) {
915
+ return null;
916
+ }
917
+
918
+ const usageChartData = {{USAGE_CHART_DATA}};
919
+ const dataIndex = params[0].dataIndex;
920
+
921
+ // Determine the split point between real and predicted data
922
+ let splitIndex = usageChartData.split_index;
923
+ if (splitIndex === undefined) {
924
+ // If not provided, assume all data is real data
925
+ splitIndex = usageChartData.x_axis.length;
926
+ }
927
+
928
+ const isInPredicatedArea = dataIndex >= splitIndex;
929
+
930
+ // Filter series based on which area we're in
931
+ const filteredParams = params.filter(item => {
932
+ if (!isInPredicatedArea) {
933
+ // In real data range, exclude predicated series
934
+ return !item.seriesName.includes('Predicated');
935
+ } else {
936
+ // In predicated data range, show all series that have values
937
+ return true;
938
+ }
939
+ });
940
+
941
+ // Build tooltip content with different background for predicted area
942
+ const bgColor = isInPredicatedArea ? '#fff8e6' : '#ffffff';
943
+ const borderColor = isInPredicatedArea ? '#ffd700' : '#ccc';
944
+ const label = isInPredicatedArea ? '<div style="font-size:10px;color:#b8860b;margin-bottom:4px;">📊 Predicted</div>' : '<div style="font-size:10px;color:#666;margin-bottom:4px;">📈 Historical Data</div>';
945
+
946
+ let content = params[0].axisValueLabel + '<br/>';
947
+ filteredParams.forEach(function(item) {
948
+ if (item.value !== null && item.value !== undefined) {
949
+ content += item.marker + ' ' + item.seriesName + ': ' + Math.round(item.value) + '<br/>';
950
+ }
951
+ });
952
+
953
+ return '<div style="background:' + bgColor + ';margin:-8px;padding:8px;border-radius:4px;border:1px solid ' + borderColor + ';">' + label + content + '</div>';
954
+ }
955
+ },
956
+ legend: {
957
+ orient: 'horizontal',
958
+ bottom: '5%',
959
+ data: [
960
+ 'Powered On VMs',
961
+ 'Consumed Sessions',
962
+ 'Spare Capacity',
963
+ 'Consumed Capacity',
964
+ "No-spare Error",
965
+ 'Spare Capacity - Predicated',
966
+ 'Consumed Capacity - Predicated',
967
+ "No-spare Error - Predicated",
968
+ 'Optimized Capacity'
969
+ ],
970
+ selected: {
971
+ 'Powered On VMs': true,
972
+ 'Consumed Capacity': true,
973
+ 'Spare Capacity': true,
974
+ 'No-spare Error': true,
975
+ 'Optimized Capacity': true,
976
+ 'Spare Capacity - Predicated': false,
977
+ 'Consumed Capacity - Predicated': false,
978
+ 'No-spare Error - Predicated': true
979
+ }
980
+ },
981
+ toolbox: {
982
+ feature: {
983
+ saveAsImage: {}
984
+ }
985
+ },
986
+ grid: {
987
+ left: '3%',
988
+ right: '4%',
989
+ bottom: '20%',
990
+ containLabel: true
991
+ },
992
+ xAxis: [
993
+ {
994
+ type: 'category',
995
+ boundaryGap: false,
996
+ data: xAxis
997
+ }
998
+ ],
999
+ yAxis: [
1000
+ {
1001
+ type: 'value'
1002
+ }
1003
+ ],
1004
+ series: [
1005
+ {
1006
+ name: 'Consumed Capacity',
1007
+ type: 'line',
1008
+ lineStyle: { width: 1 },
1009
+ symbolSize: 0,
1010
+ stack: 'Total',
1011
+ areaStyle: {},
1012
+ emphasis: {
1013
+ focus: 'none'
1014
+ },
1015
+ color: '#0a0',
1016
+ data: chartData.consumed_capacity
1017
+ },
1018
+ {
1019
+ name: 'Powered On VMs',
1020
+ type: 'line',
1021
+ lineStyle: { width: 1 },
1022
+ symbolSize: 0,
1023
+ stack: false,
1024
+ emphasis: {
1025
+ focus: 'none'
1026
+ },
1027
+ color: '#0066ff',
1028
+ data: chartData.powered_on_vms
1029
+ },
1030
+ {
1031
+ name: 'Spare Capacity',
1032
+ type: 'line',
1033
+ lineStyle: { width: 1 },
1034
+ symbolSize: 0,
1035
+ stack: 'Total',
1036
+ areaStyle: {},
1037
+ emphasis: {
1038
+ focus: 'none'
1039
+ },
1040
+ color: '#aa0',
1041
+ data: chartData.spare_capacity,
1042
+ },
1043
+ {
1044
+ name: 'No-spare Error',
1045
+ type: 'bar',
1046
+ areaStyle: {},
1047
+ emphasis: {
1048
+ focus: 'none'
1049
+ },
1050
+ color: '#a00',
1051
+ data: chartData.no_spare_error,
1052
+ },
1053
+ {
1054
+ name: 'Consumed Capacity - Predicated',
1055
+ type: 'line',
1056
+ lineStyle: { width: 1 },
1057
+ symbolSize: 0,
1058
+ stack: 'Predicated',
1059
+ areaStyle: {},
1060
+ emphasis: {
1061
+ focus: 'none'
1062
+ },
1063
+ color: '#6d6',
1064
+ data: chartData.consumed_capacity_predicated,
1065
+ },
1066
+ {
1067
+ name: 'Spare Capacity - Predicated',
1068
+ type: 'line',
1069
+ lineStyle: { width: 1 },
1070
+ symbolSize: 0,
1071
+ stack: 'Predicated',
1072
+ areaStyle: {},
1073
+ emphasis: {
1074
+ focus: 'none'
1075
+ },
1076
+ color: '#dd6',
1077
+ data: chartData.spare_capacity_predicated,
1078
+ },
1079
+ {
1080
+ name: 'No-spare Error - Predicated',
1081
+ type: 'bar',
1082
+ areaStyle: {},
1083
+ emphasis: {
1084
+ focus: 'none'
1085
+ },
1086
+ color: '#d66',
1087
+ data: chartData.no_spare_error_predicated,
1088
+ },
1089
+ {
1090
+ name: 'Optimized Capacity',
1091
+ type: 'line',
1092
+ lineStyle: { width: 1 },
1093
+ symbolSize: 0,
1094
+ emphasis: {
1095
+ focus: 'none'
1096
+ },
1097
+ color: '#6dd',
1098
+ data: chartData.optimized_capacity,
1099
+ }
1100
+ ]
1101
+ };
1102
+
1103
+ option && myChart.setOption(option);
1104
+ }
1105
+
762
1106
  // Add tab switching functionality
763
1107
  document.querySelectorAll('.tab-button').forEach(button => {
764
1108
  button.addEventListener('click', () => {
765
1109
  // Remove active class from all buttons and contents
766
1110
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
767
1111
  document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
768
-
1112
+
769
1113
  // Add active class to clicked button and corresponding content
770
1114
  button.classList.add('active');
771
1115
  document.getElementById(`${button.dataset.tab}-tab`).classList.add('active');
772
-
1116
+
773
1117
  // Update config display when switching to config tab
774
1118
  if (button.dataset.tab === 'configuration') {
775
1119
  updateConfigDisplay();
776
1120
  }
1121
+
1122
+ // Render usage chart when switching to usage tab
1123
+ if (button.dataset.tab === 'usage') {
1124
+ const timezone = document.getElementById('timezone-select').value;
1125
+ renderUsageChart(currentFilteredStartIdx, currentFilteredEndIdx, timezone);
1126
+ }
1127
+
1128
+ // Display chart data when switching to data tab
1129
+ if (button.dataset.tab === 'data') {
1130
+ displayChartData();
1131
+ }
777
1132
  });
778
1133
  });
779
1134
 
780
1135
  // Initialize timezone selector
781
1136
  document.addEventListener('DOMContentLoaded', () => {
1137
+ // Detect and display browser timezone
1138
+ const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1139
+ const browserOption = document.getElementById('browser-timezone-option');
1140
+ if (browserOption) {
1141
+ browserOption.textContent = `Browser (${browserTimezone})`;
1142
+ }
1143
+
1144
+ const initialTimezone = calendarData.timezone || 'UTC';
1145
+ renderUsageChart(null, null, initialTimezone);
782
1146
  renderChart();
783
1147
  updateConfigDisplay();
784
-
1148
+
1149
+ // Set up drag-to-select functionality once
1150
+ const overlay = document.getElementById('chart-selection-overlay');
1151
+ const usageChartEl = document.getElementById('usage-chart');
1152
+ const chartContainer = usageChartEl ? usageChartEl.parentElement : null;
1153
+ let selectionStart = null;
1154
+
1155
+ if (overlay && usageChartEl && chartContainer) {
1156
+ // Use mousedown on the chart element to start selection
1157
+ usageChartEl.addEventListener('mousedown', (e) => {
1158
+ // Only handle left mouse button
1159
+ if (e.button !== 0) return;
1160
+
1161
+ const rect = chartContainer.getBoundingClientRect();
1162
+ const x = e.clientX - rect.left;
1163
+ const y = e.clientY - rect.top;
1164
+
1165
+ // Only start selection if we're in the chart area
1166
+ if (x >= 0 && y >= 0) {
1167
+ // Enter selection mode - this will suppress tooltip
1168
+ isSelectingTimeRange = true;
1169
+ selectionStart = x;
1170
+
1171
+ // Hide any existing tooltip immediately
1172
+ if (globalUsageChart) {
1173
+ globalUsageChart.dispatchAction({
1174
+ type: 'hideTip'
1175
+ });
1176
+ }
1177
+
1178
+ overlay.style.left = selectionStart + 'px';
1179
+ overlay.style.width = '0px';
1180
+ overlay.style.display = 'block';
1181
+
1182
+ // Prevent text selection during drag
1183
+ e.preventDefault();
1184
+ }
1185
+ });
1186
+
1187
+ document.addEventListener('mousemove', (e) => {
1188
+ if (!isSelectingTimeRange) return;
1189
+
1190
+ const rect = chartContainer.getBoundingClientRect();
1191
+ const currentX = e.clientX - rect.left;
1192
+ const width = Math.abs(currentX - selectionStart);
1193
+ const left = Math.min(selectionStart, currentX);
1194
+
1195
+ overlay.style.left = left + 'px';
1196
+ overlay.style.width = width + 'px';
1197
+
1198
+ // Keep hiding tooltip during drag
1199
+ if (globalUsageChart) {
1200
+ globalUsageChart.dispatchAction({
1201
+ type: 'hideTip'
1202
+ });
1203
+ }
1204
+ });
1205
+
1206
+ document.addEventListener('mouseup', (e) => {
1207
+ if (!isSelectingTimeRange) return;
1208
+
1209
+ const rect = chartContainer.getBoundingClientRect();
1210
+ const currentX = e.clientX - rect.left;
1211
+
1212
+ // Exit selection mode
1213
+ isSelectingTimeRange = false;
1214
+ overlay.style.display = 'none';
1215
+
1216
+ // If selection is too small, treat as a click (no zoom)
1217
+ if (Math.abs(currentX - selectionStart) < 10) {
1218
+ return;
1219
+ }
1220
+
1221
+ // Get the chart container dimensions
1222
+ const chartWidth = chartContainer.offsetWidth;
1223
+
1224
+ // Estimate grid area (echarts default: left 3%, right 4%)
1225
+ const gridLeftPercent = 3;
1226
+ const gridRightPercent = 4;
1227
+ const gridLeft = (gridLeftPercent / 100) * chartWidth;
1228
+ const gridRight = (gridRightPercent / 100) * chartWidth;
1229
+ const gridWidth = chartWidth - gridLeft - gridRight;
1230
+
1231
+ // Convert pixel positions to data indices
1232
+ const normalizedStart = Math.max(0, (Math.min(selectionStart, currentX) - gridLeft) / gridWidth);
1233
+ const normalizedEnd = Math.max(0, (Math.max(selectionStart, currentX) - gridLeft) / gridWidth);
1234
+
1235
+ const usageChartData = {{USAGE_CHART_DATA}};
1236
+ const totalPoints = usageChartData.x_axis.length;
1237
+ let startIdx = Math.floor(normalizedStart * totalPoints);
1238
+ let endIdx = Math.floor(normalizedEnd * totalPoints);
1239
+
1240
+ startIdx = Math.max(0, Math.min(startIdx, totalPoints - 1));
1241
+ endIdx = Math.max(startIdx, Math.min(endIdx, totalPoints - 1));
1242
+
1243
+ // Re-render chart with selected range in current timezone
1244
+ const timezone = document.getElementById('timezone-select').value;
1245
+ renderUsageChart(startIdx, endIdx, timezone);
1246
+ });
1247
+
1248
+ // Cancel selection if mouse leaves the chart area while dragging
1249
+ chartContainer.addEventListener('mouseleave', () => {
1250
+ if (isSelectingTimeRange) {
1251
+ isSelectingTimeRange = false;
1252
+ overlay.style.display = 'none';
1253
+ }
1254
+ });
1255
+ }
1256
+
785
1257
  const timezoneSelect = document.getElementById('timezone-select');
786
1258
  timezoneSelect.value = calendarData.timezone;
787
-
1259
+
788
1260
  timezoneSelect.addEventListener('change', (e) => {
789
1261
  calendarData.timezone = e.target.value;
790
1262
  updateConfigDisplay();
1263
+
1264
+ // Refresh chart with new timezone
1265
+ renderUsageChart(currentFilteredStartIdx, currentFilteredEndIdx, e.target.value);
1266
+
1267
+ // Refresh time range display in new timezone
1268
+ const usageChartData = {{USAGE_CHART_DATA}};
1269
+ const xAxis = usageChartData.x_axis;
1270
+ const timezone = e.target.value;
1271
+
1272
+ if (currentFilteredStartIdx !== null && currentFilteredEndIdx !== null) {
1273
+ // Update for selected range
1274
+ const startIdx = Math.min(currentFilteredStartIdx, currentFilteredEndIdx);
1275
+ const endIdx = Math.max(currentFilteredStartIdx, currentFilteredEndIdx);
1276
+ const startTime = xAxis[startIdx];
1277
+ const endTime = xAxis[endIdx];
1278
+ const displayStart = convertTimeToTimezone(startTime, timezone);
1279
+ const displayEnd = convertTimeToTimezone(endTime, timezone);
1280
+ document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
1281
+ } else {
1282
+ // Update for default full range
1283
+ const startTime = xAxis[0];
1284
+ const endTime = xAxis[xAxis.length - 1];
1285
+ const displayStart = convertTimeToTimezone(startTime, timezone);
1286
+ const displayEnd = convertTimeToTimezone(endTime, timezone);
1287
+ document.getElementById('selection-info').textContent = `${displayStart} → ${displayEnd}`;
1288
+ }
1289
+ });
1290
+
1291
+ // Add reset zoom button functionality
1292
+ const resetButton = document.getElementById('reset-chart-zoom');
1293
+ resetButton.addEventListener('click', () => {
1294
+ renderUsageChart();
791
1295
  });
792
1296
 
793
1297
  // Add copy to clipboard functionality
@@ -804,6 +1308,24 @@
804
1308
  console.error('Failed to copy text: ', err);
805
1309
  });
806
1310
  });
1311
+
1312
+ // Add copy to clipboard functionality for data tab
1313
+ const copyDataButton = document.getElementById('copy-data');
1314
+ if (copyDataButton) {
1315
+ copyDataButton.addEventListener('click', () => {
1316
+ const usageChartData = {{USAGE_CHART_DATA}};
1317
+ const jsonString = JSON.stringify(usageChartData, null, 2);
1318
+ navigator.clipboard.writeText(jsonString).then(() => {
1319
+ const originalText = copyDataButton.textContent;
1320
+ copyDataButton.textContent = 'Copied!';
1321
+ setTimeout(() => {
1322
+ copyDataButton.textContent = originalText;
1323
+ }, 2000);
1324
+ }).catch(err => {
1325
+ console.error('Failed to copy text: ', err);
1326
+ });
1327
+ });
1328
+ }
807
1329
  });
808
1330
  </script>
809
1331